这个脚本比较小众,能够实现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);
}
});
})();