B站自动点赞 (模块化增强版)

B站视频/番剧自动点赞。强化模块化设计,完善错误处理,优化选择器策略

// ==UserScript==
// @name         B站自动点赞 (模块化增强版)
// @namespace    http://tampermonkey.net/
// @version      2025.06.21
// @description  B站视频/番剧自动点赞。强化模块化设计,完善错误处理,优化选择器策略
// @author       Gemini (重构优化)
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/bangumi/play/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // -------------------------
    // 1. 核心配置模块
    // -------------------------
    const CONFIG = {
        // 触发点赞的条件配置
        PLAY_PROGRESS_THRESHOLD: 30,       // 播放进度百分比阈值
        PLAY_TIME_THRESHOLD: 60,           // 播放时间阈值(秒)

        // 运行时控制参数
        CHECK_INTERVAL: 1500,              // 状态检查间隔(毫秒)
        INIT_DELAY: 1500,                  // 初始化延迟(毫秒)
        ACTION_DELAY: 200,                 // 操作反馈延迟(毫秒)
        POST_ACTION_DELAY: 700,            // 操作后检查延迟(毫秒)

        // 调试与日志配置
        DEBUG_MODE: true,                  // 调试模式开关
        LOG_LEVEL: 'INFO'                 // 日志级别: DEBUG/INFO/WARN/ERROR
    };

    // -------------------------
    // 2. 全局状态管理
    // -------------------------
    let state = {
        checkInterval: null,       // 进度检查定时器ID
        currentUrl: '',            // 当前页面URL
        isLiked: false,            // 当前点赞状态
        pageType: 'unknown'        // 页面类型标识
    };

    // -------------------------
    // 3. 日志系统模块
    // -------------------------
    /**
     * 统一日志输出接口
     * @param {string} level - 日志级别: DEBUG/INFO/WARN/ERROR
     * @param {string} message - 日志内容
     */
    function log(level, message) {
        if (!CONFIG.DEBUG_MODE && level !== 'ERROR') return;

        const levels = {
            DEBUG: '\x1b[36m[DEBUG]\x1b[0m',
            INFO: '\x1b[32m[INFO]\x1b[0m',
            WARN: '\x1b[33m[WARN]\x1b[0m',
            ERROR: '\x1b[31m[ERROR]\x1b[0m'
        };

        if (levels[level]) {
            console.log(`[BiliAutoLike] ${new Date().toLocaleTimeString()} ${levels[level]} - ${message}`);
        }
    }

    // -------------------------
    // 4. 页面类型识别模块
    // -------------------------
    const PAGE_TYPES = {
        VIDEO: 'video',
        BANGUMI: 'bangumi',
        UNKNOWN: 'unknown'
    };

    /**
     * 获取当前页面类型
     * @returns {string} 页面类型常量
     */
    function getPageType() {
        const href = window.location.href;
        const isBangumi = href.includes('/bangumi/play/');
        const isVideo = href.includes('/video/');

        state.pageType = isBangumi ? PAGE_TYPES.BANGUMI : isVideo ? PAGE_TYPES.VIDEO : PAGE_TYPES.UNKNOWN;
        log('INFO', `识别页面类型: ${state.pageType}`);
        return state.pageType;
    }

    // -------------------------
    // 5. 选择器策略管理模块
    // -------------------------
    /**
     * 点赞按钮选择器策略集合
     * 按优先级顺序排列选择器
     */
    const likeButtonStrategies = {
        [PAGE_TYPES.VIDEO]: [
            '.bpx-player-ctrl .bpx-player-ctrl-like',      // 新版播放器点赞按钮
            'div[title="点赞(Q)"]',                      // 标题属性选择器
            '.video-toolbar-left-item.video-like',         // 视频工具栏点赞
            'button[aria-label="点赞"]'                    // 无障碍属性选择器
        ],
        [PAGE_TYPES.BANGUMI]: [
            '.ep-handle-like',                             // 新版番剧点赞按钮
            '.bangumi-player-container .like-btn',         // 番剧播放器内按钮
            '#like_info',                                  // 旧版ID选择器
            'button[class*="bangumi-player-like"]'         // 类名模糊匹配
        ],
        [PAGE_TYPES.UNKNOWN]: [
            'button[aria-label="点赞"]',                   // 通用无障碍选择器
            '.like-button',                                // 通用类名选择器
            '.bpx-player-ctrl-like'                        // 尝试通用播放器选择器
        ]
    };

    /**
     * 获取点赞按钮元素
     * @returns {HTMLElement|null} 点赞按钮元素或null
     */
    function getLikeButton() {
        const pageType = getPageType();
        const selectors = likeButtonStrategies[pageType] || likeButtonStrategies[PAGE_TYPES.UNKNOWN];

        log('DEBUG', `为${pageType}页面查找点赞按钮,尝试选择器: ${selectors.join(', ')}`);

        for (const selector of selectors) {
            const element = document.querySelector(selector);
            if (element && isElementVisible(element)) {
                log('INFO', `找到点赞按钮,选择器: ${selector}`);
                return element;
            }
        }

        log('WARN', `未找到可见的点赞按钮,页面类型: ${pageType}`);
        return null;
    }

    // -------------------------
    // 6. DOM状态检测模块
    // -------------------------
    /**
     * 检测元素是否可见
     * @param {HTMLElement} element 目标元素
     * @returns {boolean} 元素可见性状态
     */
    function isElementVisible(element) {
        if (!element) return false;

        const style = window.getComputedStyle(element);
        const rect = element.getBoundingClientRect();

        return (
            style.display !== 'none' &&
            style.visibility !== 'hidden' &&
            style.opacity > 0 &&
            rect.height > 0 &&
            rect.width > 0
        );
    }

    /**
     * 检测视频是否已点赞
     * @param {HTMLElement} button 点赞按钮元素
     * @returns {boolean} 点赞状态
     */
    function isVideoLiked(button) {
        if (!button) return true; // 找不到按钮时假设已点赞避免重复操作

        const activeStates = [
            'on', 'actived', 'is-liked', 'liked', 'active', 'zaned',
            'bili-__heart-on', 'bili-__heart-active' // 补充B站新状态类名
        ];

        for (const stateClass of activeStates) {
            if (button.classList.contains(stateClass)) {
                log('DEBUG', `检测到已点赞状态类: ${stateClass}`);
                return true;
            }
        }

        // 检测无障碍属性
        if (button.getAttribute('aria-checked') === 'true' ||
            button.getAttribute('aria-pressed') === 'true') {
            log('DEBUG', '检测到已点赞无障碍属性');
            return true;
        }

        // 检测子元素状态
        const likedIndicators = [
            '.icon-liked', '.bili-__icon-heart-fill',
            '[class*="icon_like_selected"]', '.liked-icon'
        ];

        for (const selector of likedIndicators) {
            if (button.querySelector(selector)) {
                log('DEBUG', `子元素检测到点赞状态: ${selector}`);
                return true;
            }
        }

        return false;
    }

    // -------------------------
    // 7. 点赞操作执行模块
    // -------------------------
    /**
     * 执行点赞操作
     * @param {HTMLElement} button 点赞按钮元素
     */
    function likeVideo(button) {
        if (isVideoLiked(button)) {
            log('INFO', '目标已点赞,跳过操作');
            stopProgressCheck();
            return;
        }

        log('INFO', '开始执行点赞操作');
        const pageType = getPageType();
        const strategy = getClickStrategy(pageType);

        try {
            // 添加视觉反馈
            button.style.transition = 'transform 0.15s ease';
            button.style.transform = 'scale(1.2)';

            setTimeout(() => {
                // 执行对应页面的点击策略
                strategy(button);

                // 恢复按钮样式
                button.style.transform = '';
                log('INFO', '点赞操作事件已派发');

                // 延迟检查点赞结果
                setTimeout(checkLikeResult, CONFIG.POST_ACTION_DELAY);
            }, CONFIG.ACTION_DELAY);

        } catch (error) {
            log('ERROR', `点赞操作异常: ${error.message}`);
            button.style.transform = '';
            stopProgressCheck();
        }
    }

    /**
     * 获取点击策略函数
     * @param {string} pageType 页面类型
     * @returns {Function} 点击策略函数
     */
    function getClickStrategy(pageType) {
        switch (pageType) {
            case PAGE_TYPES.VIDEO:
                return videoClickStrategy;
            case PAGE_TYPES.BANGUMI:
                return bangumiClickStrategy;
            default:
                return defaultClickStrategy;
        }
    }

    /**
     * 普通视频点击策略
     * @param {HTMLElement} button 点赞按钮
     */
    function videoClickStrategy(button) {
        log('DEBUG', '执行视频页面点击策略: mouseover -> mousedown -> mouseup -> click');
        dispatchEvents(button, ['mouseover', 'mousedown', 'mouseup', 'click']);
    }

    /**
     * 番剧页面点击策略
     * @param {HTMLElement} button 点赞按钮
     */
    function bangumiClickStrategy(button) {
        log('DEBUG', '执行番剧页面点击策略: mousedown -> mouseup');
        dispatchEvents(button, ['mousedown', 'mouseup']);
    }

    /**
     * 默认点击策略
     * @param {HTMLElement} button 点赞按钮
     */
    function defaultClickStrategy(button) {
        log('DEBUG', '执行默认点击策略: 直接click()');
        button.click();
    }

    /**
     * 派发事件序列
     * @param {HTMLElement} element 目标元素
     * @param {string[]} eventTypes 事件类型数组
     */
    function dispatchEvents(element, eventTypes) {
        eventTypes.forEach(type => {
            const event = new MouseEvent(type, {
                bubbles: true,
                cancelable: true,
                view: window
            });
            log('DEBUG', `派发事件: ${type}`);
            element.dispatchEvent(event);
        });
    }

    /**
     * 检查点赞结果
     */
    function checkLikeResult() {
        const button = getLikeButton();
        if (button && isVideoLiked(button)) {
            log('INFO', '点赞成功,按钮状态已更新');
        } else {
            log('WARN', '点赞可能未成功,按钮状态未改变');
            log('DEBUG', '建议检查选择器是否匹配当前页面版本');
        }
        stopProgressCheck();
    }

    // -------------------------
    // 8. 播放进度监控模块
    // -------------------------
    /**
     * 检查播放进度并触发点赞
     */
    function checkPlayProgress() {
        const video = document.querySelector('video');
        if (!video) {
            log('WARN', '未找到视频播放元素');
            return;
        }

        // 跳过未准备好的视频
        if (video.paused || video.ended || video.readyState < 2) {
            return;
        }

        const currentTime = video.currentTime;
        const duration = video.duration;

        // 过滤无效视频
        if (!duration || duration === Infinity || duration < 5) {
            log('DEBUG', '视频时长无效或过短');
            return;
        }

        const progressPercent = (currentTime / duration) * 100;
        const meetProgress = progressPercent >= CONFIG.PLAY_PROGRESS_THRESHOLD;
        const meetTime = currentTime >= CONFIG.PLAY_TIME_THRESHOLD;

        if (meetProgress || meetTime) {
            log('INFO', `满足点赞条件 - 进度: ${progressPercent.toFixed(1)}% (阈值: ${CONFIG.PLAY_PROGRESS_THRESHOLD}%) ` +
                        `或 时间: ${currentTime.toFixed(1)}s (阈值: ${CONFIG.PLAY_TIME_THRESHOLD}s)`);

            const button = getLikeButton();
            if (button) {
                likeVideo(button);
            } else {
                log('WARN', '满足点赞条件但未找到点赞按钮,停止检查');
                stopProgressCheck();
            }
        }
    }

    /**
     * 启动进度检查
     */
    function startProgressCheck() {
        if (state.checkInterval) return;

        log('INFO', `启动播放进度检查,间隔: ${CONFIG.CHECK_INTERVAL}ms`);
        state.checkInterval = setInterval(checkPlayProgress, CONFIG.CHECK_INTERVAL);
    }

    /**
     * 停止进度检查
     */
    function stopProgressCheck() {
        if (state.checkInterval) {
            clearInterval(state.checkInterval);
            state.checkInterval = null;
            log('INFO', '停止播放进度检查');
        }
    }

    // -------------------------
    // 9. 脚本生命周期管理模块
    // -------------------------
    /**
     * 初始化脚本模块
     */
    function initialize() {
        stopProgressCheck();

        const video = document.querySelector('video');
        const button = getLikeButton();

        if (video && button) {
            log('INFO', '脚本初始化完成,视频元素和点赞按钮已就绪');

            if (isVideoLiked(button)) {
                log('INFO', '目标已点赞,无需启动检查');
                return;
            }

            startProgressCheck();
        } else {
            log('WARN', '初始化失败,缺少关键元素');
            if (!video) log('DEBUG', '未找到<video>元素');
            if (!button) log('DEBUG', '未找到点赞按钮');
        }
    }

    /**
     * 主执行函数
     */
    function main() {
        // 检测URL变化
        if (window.location.href === state.currentUrl && state.checkInterval) {
            log('DEBUG', 'URL未变化,跳过重复初始化');
            return;
        }

        state.currentUrl = window.location.href;
        log('INFO', `脚本启动/页面变更 - URL: ${state.currentUrl}, 类型: ${getPageType()}`);

        stopProgressCheck();

        // 延迟初始化,等待页面资源加载
        setTimeout(initialize, CONFIG.INIT_DELAY);
    }

    // -------------------------
    // 10. 启动脚本
    // -------------------------
    log('INFO', 'B站自动点赞脚本(模块化增强版)已加载,等待页面初始化...');

    // 页面加载完成事件
    window.addEventListener('load', main);

    // 监听SPA路由变化
    const originalPushState = history.pushState;
    history.pushState = function() {
        const result = originalPushState.apply(this, arguments);
        log('DEBUG', '检测到history.pushState,重新初始化脚本');
        main();
        return result;
    };

    const originalReplaceState = history.replaceState;
    history.replaceState = function() {
        const result = originalReplaceState.apply(this, arguments);
        log('DEBUG', '检测到history.replaceState,重新初始化脚本');
        main();
        return result;
    };

    // DOM变化观察器
    const observer = new MutationObserver(mutations => {
        if (!state.checkInterval) {
            log('DEBUG', 'DOM变化检测,尝试初始化脚本');
            initialize();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true
    });

    // 处理文档已加载情况
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        main();
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址