上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

一个小众刷课脚本(81联聘)

[复制链接]
  • TA的每日心情
    奋斗
    昨天 20:01
  • 签到天数: 1 天

    [LV.1]初来乍到

    2

    主题

    0

    回帖

    6

    积分

    助理工程师

    积分
    6
    发表于 昨天 22:19 | 显示全部楼层 | 阅读模式
    安装此脚本 如何安装? 脚本问题反馈 给脚本评分 查看代码

    这个脚本比较小众,能够实现81联聘视频课的自动播放。

    在测试过程中也是本想开发一个一键操作全程无需人工参与,但试了几次效果也不是很好,

    所以改为在视频页扫描所有非100%的课程,并通过点击扫描到的第一课开始播放所有未完成的课程。

    因为81联聘是页面加载课程,不加载扫描不到,所以,我添加了自动下拉页面的功能,保证能够扫描到所有的课程,同时,因为视频是挂载类,链接阿里云服务,不是正常的数据链接,有时会扫描不到VIDEO,所以改动了一下传统逻辑,保证视频能够正常播放。

    由于HTML结果每各页面都不同,尝试做联系,但最终失败了,所以只做了单个页面的扫描,无法实现一键学完全部课程。脚本包含了完善的错误处理机制和模态框关闭逻辑.。主要是针对无法关闭视频模态框问题进行了处理,不关闭模态窗连续播放不算播放时长。

    大神可以给个意见,下边附上原代码,复制到插件就可以使用,

    当然,注意一下网址,应该是不用改动!!!这是安装地址    https://scriptcat.org/zh-CN/script-show-page/4201

    // ==UserScript==
    // @name 81联聘-批量自动学完助手 v1.2
    // @namespace https://github.com/yourname
    // @version 1.2
    // @description 修复稳定性问题,增强错误处理和元素检测
    // @author You
    // @match https://learn.81lianpin.com/myCourses/details/*
    // @grant none
    // ==/UserScript==

    / 让页面自身的未捕获异常不再抛到控制台(仅视觉清爽,不影响功能) /
    window.addEventListener('error', e => {
    if (e.filename?.includes('index.vue')) e.preventDefault();
    });

    (() => {
    'use strict';
    const log = (...a) => console.log('[81批量助手]', ...a);
    const sleep = t => new Promise(r => setTimeout(r, t));
    let isProcessing = false;
    let isCompletedAlertShown = false; // 用于控制完成提示只显示一次

    /* -------------------- 路由 -------------------- */
    const onList = () => location.pathname === '/myCourses';
    const onChapter = () => location.pathname.includes('/myCourses/details/');
    
    /* -------------------- 面板 -------------------- */
    
    function drawPanel(list) {
        const old = document.getElementById('lp-helper-panel');
        if (old) old.remove();
    
        const box = document.createElement('div');
        box.id = 'lp-helper-panel';
        box.innerHTML = `
      <style>
        #lp-helper-panel{position:fixed;right:20px;top:100px;width:280px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.15);z-index:9999;font-size:14px;font-family:Arial;}
        #lp-helper-panel h4{margin:0;padding:10px;background:#1890ff;color:#fff;border-radius:6px 6px 0 0;}
        #lp-helper-panel ul{margin:0;padding:10px;list-style:none;max-height:300px;overflow:auto;}
        #lp-helper-panel li{margin-bottom:6px;}
        #lp-helper-panel .btn{background:#52c41a;color:#fff;padding:4px 8px;border:none;border-radius:4px;cursor:pointer;margin-left:6px;}
        #lp-helper-panel .reshow{background:#fa8c16;}
        #lp-helper-panel .processing{opacity:0.7;pointer-events:none;}
      </style>
      <h4>未学完小节(共 ${list.length} 个)</h4>
      <ul>${list.map((it, i) => `<li>${it.title} <button class="btn" data-index="${i}">单独学</button></li>`).join('')}</ul>
      <div style="padding:10px;border-top:1px solid #eee">
        <button id="lp-rescan" class="btn reshow">🔄 重新扫描</button>
        <span id="lp-countdown" style="margin-left:10px;color:#666"></span>
      </div>`;
        document.body.appendChild(box);
    
        /* 单独学 */
        box.querySelectorAll('.btn:not([id])').forEach(b => {
            b.onclick = () => handleOne(list[+b.dataset.index]);
        });
    
        /* 重新扫描 */
        box.querySelector('#lp-rescan').onclick = () => {
            box.remove();
            main();
        };
    }
    
    /* -------------------- 单个小节 -------------------- */
    async function handleOne(item) {
        log('进入小节', item.title);
    
        // 滚动到元素并点击
        item.node.scrollIntoView({ behavior: 'smooth', block: 'center' });
        await sleep(800);
    
        try {
            item.node.click();
        } catch (e) {
            log('点击元素失败:', e);
            // 尝试其他方式触发点击
            const event = new MouseEvent('click', {
                view: window,
                bubbles: true,
                cancelable: true
            });
            item.node.dispatchEvent(event);
        }
    
        /* 改进的视频检测机制 */
        let video = null;
        const maxWaitTime = 15000; // 15秒超时
        const startTime = Date.now();
    
        while (!video && Date.now() - startTime < maxWaitTime) {
            video = document.querySelector('video');
            if (video) break;
            await sleep(500);
        }
    
        if (!video) {
            log('未检测到 <video>,跳过本节', 'error');
            // 尝试关闭可能弹出的任何模态框
            tryCloseModal();
            return;
        }
    
        await waitVideoDone(video);
        await sleep(5000); // 视频播放完毕后等待5秒
    
        // 重新扫描并点击第一个“单独学”按钮
        main(() => {
            const panel = document.getElementById('lp-helper-panel');
            if (panel) {
                const firstButton = panel.querySelector('.btn');
                if (firstButton) {
                    firstButton.click();
                } else if (!isCompletedAlertShown) {
                    alert('您的课程已完成');
                    isCompletedAlertShown = true; // 标记提示已显示
                }
            }
        });
    }
    
    /* -------------------- 等待视频结束 -------------------- */
    function waitVideoDone(video) {
        return new Promise(async (resolve) => {
            const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
    
            // 确保视频可以播放
            video.muted = true;
            try {
                await video.play();
            } catch (e) {
                log('自动播放失败,尝试用户交互模拟:', e);
                // 模拟用户点击视频以启动播放
                video.dispatchEvent(new Event('click'));
                await sleep(1000);
            }
    
            /* 1. 前 10-20 秒正常播放 */
            const firstEnd = rand(10, 20);
            video.playbackRate = 1;
    
            log(`前 ${firstEnd} 秒正常播放`);
    
            // 使用timeupdate事件监听进度
            const progressHandler = () => {
                if (video.currentTime >= firstEnd) {
                    video.removeEventListener('timeupdate', progressHandler);
    
                    /* 2. 跳到最后部分加速播放 */
                    const targetTime = Math.max(video.duration - 15, 0);
                    video.currentTime = targetTime;
                    video.playbackRate = 1;
    
                    log(`跳转到最后部分,1 倍速播放`);
                }
            };
    
            video.addEventListener('timeupdate', progressHandler);
    
            // 添加结束事件监听
            video.addEventListener('ended', async () => {
                /* 缓冲 3 秒 */
                await sleep(3000);
    
                /* 尝试多种方式关闭模态框 */
                await tryCloseModal();
    
                /* 等待页面稳定 */
                await sleep(3000);
    
                resolve();
            }, { once: true });
    
            // 添加超时机制,防止视频卡住
            setTimeout(() => {
                log('视频处理超时,尝试继续下一步');
                resolve();
            }, 300000); // 5分钟超时
        });
    }
    
    /* -------------------- 尝试关闭模态框 -------------------- */
    async function tryCloseModal() {
        // 多种关闭方式尝试
        const closeMethods = [
            // XPath方式
            () => {
                const closeBtn = document.evaluate(
                    '//*[@id="content"]/img',
                    document,
                    null,
                    XPathResult.FIRST_ORDERED_NODE_TYPE,
                    null
                ).singleNodeValue;
                if (closeBtn) {
                    closeBtn.click();
                    return true;
                }
                return false;
            },
            // 类名方式
            () => {
                const closeBtn = document.querySelector('.close, .ant-modal-close, [aria-label="Close"]');
                if (closeBtn) {
                    closeBtn.click();
                    return true;
                }
                return false;
            },
            // ESC键模拟
            () => {
                const escEvent = new KeyboardEvent('keydown', {
                    key: 'Escape',
                    code: 'Escape',
                    keyCode: 27,
                    which: 27,
                    bubbles: true
                });
                document.dispatchEvent(escEvent);
                return true;
            },
            // 返回上一页
            () => {
                history.back();
                return true;
            }
        ];
        for (const method of closeMethods) {
            try {
                if (method()) {
                    log('成功关闭模态框');
                    await sleep(2000);
                    return;
                }
            } catch (e) {
                log('关闭模态框方法失败:', e);
            }
            await sleep(1000);
        }
    
        log('所有关闭方法都失败了');
    }
    
    /* -------------------- 扫描未学完 -------------------- */
    function main(callback) {
        // 清除之前的定时器
        if (window.lpScanInterval) {
            clearInterval(window.lpScanInterval);
        }
    
        // 设置新的定时器
        window.lpScanInterval = setInterval(() => {
            // 如果正在处理中,跳过扫描
            if (isProcessing) return;
    
            const rows = Array.from(document.querySelectorAll('span'))
            .filter(s => {
                const text = s.textContent;
                return text.includes('已学:') && !text.includes('已学:100%');
            })
            .map(s => {
                const container = s.closest('div[class*="row"], div[class*="item"], li, [class*="chapter"]') || s.parentElement;
                const clickable = container.querySelector('a, button, [onclick], [data-url]') || container;
    
                // 改进标题提取
                let title = '未知小节';
                try {
                    title = container.textContent.trim()
                        .split('\n')
                        .map(line => line.trim())
                        .find(line => line.length > 0 && !line.includes('已学:')) || title;
                } catch (e) {
                    log('提取标题失败:', e);
                }
    
                return {
                    title: title,
                    node: clickable
                };
            });
    
            if (rows.length > 0) {
                clearInterval(window.lpScanInterval);
                drawPanel(rows);
                if (callback) {
                    callback();
                }
            } else if (!isCompletedAlertShown) {
                alert('您的课程已完成');
                isCompletedAlertShown = true; // 标记提示已显示
            }
        }, 2000);
    }
    
    /* -------------------- 入口 -------------------- */
    if (onChapter()) {
        // 等待页面完全加载
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', main);
        } else {
            main();
        }
    
        // 监听路由变化
        let lastUrl = location.href;
        const observer = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                if (onChapter()) {
                    // 延迟一下确保页面内容加载
                    setTimeout(main, 1000);
                }
            }
        });
    
        observer.observe(document, { subtree: true, childList: true });
    }
    
    // 添加一行代码,确保脚本在后台也能继续运行
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
            // 当页面被最小化或切换到后台时,保持脚本活动
            setTimeout(() => {
                if (document.visibilityState === 'hidden') {
                    main();
                }
            }, 1000);
        }
    });

    })();

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表