0%

js刷课脚本实践

某天拿到了一个刷课的网站,需要帮忙刷一下课,但是呢这个网站

  • 打开之后不自动播放
  • 播放完一个视频不会自动播放下一个
  • 不能直接倍速
  • 不能拖动(这个解决不了QAQ)
  • 检测出倍速需要点击弹出框来关闭,从而继续刷课
  • 切换一个课程后有时候会弹出一个alert点击来播放视频

嗯……就这么多破事儿

实现

实现方法

油猴(篡改猴)扩展程序

自动播放视频

一开始的思路是直接

1
document.querySelector('video').play

让视频播放。

但是在网页的控制台中这个方法可以,但是写到油猴脚本里没法运行,还是不能播放视频。因此使用浏览器模拟点击的方法。

在点击之前,首先要找到video视频标签,这个网页的video元素使用的应该是用js动态加载出来的,所以监听页面加载完毕的方法判断video元素加载到了页面上不太好用(实际上确实不咋好用),于是使用递归查找video元素

查找页面中的video元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检查页面内是否有video元素
function hasVideo() {
if (!document.querySelector('video')) {
// 延时100毫秒再次检查
setTimeout(hasVideo, 500);
console.log('再次检查');
} else {
// 判断video元素内的视频是否在播放
const currentVideo = document.querySelector('video');
if (currentVideo.paused) {
console.log('当前视频未播放,开始自动播放...');
setTimeout(() => {
checkVideoPlaying();
}, 500);
}
}
}

如果视频未开始播放,那就调用checkVideoPlaying();函数

检查视频是否未播放

1
2
3
4
5
6
7
8
9
10
11
12
13
// 检查视频是否在播放
function checkVideoPlaying() {
//console.log('checkVideoPlaying调用');
//const currentVideo = document.querySelector('video');
setTimeout(() => {
// document.querySelector('video').play()
simulateClickOnVideo();
console.log('当前视频开始播放');
}, 200);
setTimeout(() => {
checkPlaybackRate();// 检测视频是否为2倍速播放
}, 500);
}

模拟点击一下video元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function simulateClickOnVideo() {
const video = document.querySelector('video');
if (video) {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
video.dispatchEvent(clickEvent);
console.log('模拟点击 video 元素');
} else {
console.log('未找到 video 元素');
}
}

倍速

检查视频是否未播放的时候,里面写了一个函数,checkPlaybackRate();检测视频是否为2倍速播放

先让视频两倍速播放,如果不是两倍速重新检查

网站支持两倍速,但是有时候他还是会检测出倍速然后暂停…………解决方法见6. 浏览器模拟点击弹出的按钮

1
2
3
4
5
6
7
8
9
10
11
// 检测视频是否为2倍速播放
function checkPlaybackRate() {
document.querySelector('video').playbackRate = 2;
const currentVideo = document.querySelector('video');
if (currentVideo.playbackRate == 2) {
console.log('2倍速播放成功')
} else {
setTimeout(checkPlaybackRate, 500);
}
checkCourseLearned();
}

学完自动播放下一个视频

检测是否学完

这里就要按照这个网站的特征来分析了

image-20240801234544151

把鼠标放在这个对钩上面发现显示出了“已学完”

image-20240801234701881

这样的是“进行中”

image-20240801234727101

这样的是“未开始”

所以从这里入手分析

image-20240801234849537

选中这个元素发现里面有一个<i>标签,里面的title是进行中,所以就判断这个title是不是已学完就可以了

同时发现正在播放的视频这里有一个active,没有播放的视频没有这个带active的类,所以就寻找这个带active的类,看这个类下面的<i>标签中的title是否是“已学完”。

image-20240801235841750

image-20240802000239250

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 检测本节课是否学过
function checkCourseLearned() {
console.log('checkCourseLearned调用');
// 查找所有具有指定类名的元素
const items = document.querySelectorAll('.resource-item.resource-item-train.resource-item-active');

// 遍历这些元素
items.forEach(item => {
// 查找 <i> 标签
const iElement = item.querySelector('i');

// 检查 <i> 标签的 title 属性是否为 "已学完"
if (iElement && iElement.title === '已学完') {
console.log('找到一个已学完的资源项:', item);
// 如果这节课学完了,就播放下一节课
playNextVideo();
} else {
//如果这节课没学完,监听视频播放结束事件
listenVideoEnd();
}
});
}

监听视频是否播放完

1
2
3
4
5
6
7
8
9
10
// 监听视频播放结束事件
function listenVideoEnd() {
const currentVideo = document.querySelector('video');
console.log('listenVideoEnd调用');
// 监听视频播放结束事件
currentVideo.addEventListener('ended', function () {
console.log('当前视频播放结束,正在尝试播放下一个视频...');
playNextVideo();
});
}

播放下一个视频

首先要找到当前激活的项const activeItem = document.querySelector('.resource-item.resource-item-train.resource-item-active');,然后找到下一个没有激活的项,也就是.resource-item.resource-item-train,找到之后就点击这个项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 播放下一个视频
function playNextVideo() {
console.log('playNextVideo调用');
// 找到当前激活的资源项
const activeItem = document.querySelector('.resource-item.resource-item-train.resource-item-active');
if (!activeItem) {
console.log('没有找到当前激活的资源项');
return;
} else {
console.log(activeItem)
}

// 找到下一个资源项
let nextItem = activeItem.nextElementSibling;
while (nextItem && (!nextItem.classList.contains('resource-item') || !nextItem.classList.contains('resource-item-train'))) {
nextItem = nextItem.nextElementSibling;
}
if (!nextItem) {
// 如果已经是最后一个,循环回到第一个
nextItem = activeItem.parentNode.querySelector('.resource-item.resource-item-train');
}

if (nextItem) {
// 模拟点击下一个资源项
simulateClick(nextItem, function () {
// 延时处理alert并播放视频
setTimeout(handleAlertAndPlayVideo, 3000);
});
} else {
console.log('没有找到下一个资源项或资源项不符合条件');
}

hasVideo();
}

模拟点击

1
2
3
4
5
6
7
8
9
10
11
// 模拟点击
function simulateClick(element, callback) {
const clickEvent = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
element.dispatchEvent(clickEvent);
// 如果需要执行回调函数,确保点击事件处理完毕后执行
setTimeout(callback, 0);
}

处理alert

这个可能不会生效……所以后面又写了一个直接浏览器模拟点击按钮的

1
2
3
4
5
6
7
8
9
10
// 处理alert并播放视频
function handleAlertAndPlayVideo() {
// 这里假设alert是点击后弹出的,并且需要用户确认
if (window.alert) {
// 模拟点击alert的确定按钮
window.alert = function (message) {
console.log('模拟点击alert确定按钮');
};
}
}

浏览器模拟点击弹出的按钮

image-20240802001050196

经分析,所有的弹出按钮都有这个类

但是运行的时候发现,一打开脚本就跳转到搜索界面,所以肯定是某个搜索的地方也有这个类

image-20240802001320139

果不其然,这个搜索图标也使用了这个类,所以再排除这个fish-input-search-button类就好了

这里是怎么发现的呢…………本来这个课是放在虚拟机里刷的,因为不能离开这个网页,所以网页比例就没调,但是在某一瞬间缩小了一下比例,本来是只有一个搜索按钮的地方突然变出来一个搜索框,由于之前比例太大这个搜索框没显示,但是现在显示了,所以多了这么一个类,于是就需要排除这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function simulateClickOnPrimaryButton() {
const button = document.querySelector('.fish-btn.fish-btn-primary');
if (button) {
const classList = button.classList;
if (classList.contains('fish-input-search-button')) {
console.log('按钮包含 fish-input-search-button 类,不进行点击');
} else {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
button.dispatchEvent(clickEvent);
console.log('模拟点击 fish-btn fish-btn-primary 按钮');
}
} else {
console.log('未找到 fish-btn fish-btn-primary 按钮');
}
}

播放完的Bug

有时候视频播放完会出来一个Bug,就是视频不自动暂停,这时候如果用鼠标点一下暂停键,脚本才会检测到video.ended,所以再加一个判断,判断视频已播放时长是否等于总时长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 判断视频播放时长是否等于总时长
function isVideoDurationEqual() {
const video = document.querySelector('video');
if (video) {
const currentTime = video.currentTime;
const duration = video.duration;
const tolerance = 1; // 允许的误差范围,单位为秒

if (Math.abs(currentTime - duration) <= tolerance) {
console.log('视频播放时长等于总时长');
setTimeout(() => {
// 点击video元素,触发视频暂停
simulateClickOnVideo();
}, 3000);
}
}
}

持续执行的函数

1
2
3
setInterval(simulateClickOnPrimaryButton, 3000);
setInterval(checkPlaybackRate, 3000);
setInterval(isVideoDurationEqual, 3000);

这三个函数让他每隔三秒重复执行就好了。其他的整个代码逻辑使用递归的方法让整个脚本持续运行

总代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// ==UserScript==
// @name AutoPlay
// @namespace https://creeeeeeeeeeper.github.io/
// @version 1.0
// @description Automatically play the next video in the video list.
// @author ZYG
// @match *://*/*
// @grant none
// @run-at document-idle
// ==/UserScript==

(function () {
'use strict';
console.log('Autoplay js start');
hasVideo();

function playv() {
document.querySelector('video').play()
}

setInterval(playv, 1000);
setInterval(simulateClickOnPrimaryButton, 3000);
setInterval(checkPlaybackRate, 3000);
setInterval(isVideoDurationEqual, 3000);

function simulateClickOnPrimaryButton() {
const button = document.querySelector('.fish-btn.fish-btn-primary');
if (button) {
const classList = button.classList;
if (classList.contains('fish-input-search-button')) {
console.log('按钮包含 fish-input-search-button 类,不进行点击');
} else {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
button.dispatchEvent(clickEvent);
console.log('模拟点击 fish-btn fish-btn-primary 按钮');
}
} else {
console.log('未找到 fish-btn fish-btn-primary 按钮');
}
}

// 检查页面内是否有video元素
function hasVideo() {
if (!document.querySelector('video')) {
// 延时100毫秒再次检查
setTimeout(hasVideo, 500);
console.log('再次检查');
} else {
// 判断video元素内的视频是否在播放
const currentVideo = document.querySelector('video');
if (currentVideo.paused) {
console.log('当前视频未播放,开始自动播放...');
setTimeout(() => {
checkVideoPlaying();
}, 500);
}
}
}

// 检查视频是否在播放
function checkVideoPlaying() {
console.log('checkVideoPlaying调用');
//const currentVideo = document.querySelector('video');
setTimeout(() => {
// document.querySelector('video').play()
simulateClickOnVideo();
console.log('当前视频开始播放');
}, 200);
setTimeout(() => {
checkPlaybackRate();// 检测视频是否为2倍速播放
}, 500);
}

function simulateClickOnVideo() {
const video = document.querySelector('video');
if (video) {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
video.dispatchEvent(clickEvent);
console.log('模拟点击 video 元素');
} else {
console.log('未找到 video 元素');
}
}

// 检测视频是否为2倍速播放
function checkPlaybackRate() {
document.querySelector('video').playbackRate = 2;
const currentVideo = document.querySelector('video');
if (currentVideo.playbackRate == 2) {
console.log('2倍速播放成功')
} else {
setTimeout(checkPlaybackRate, 500);
}
checkCourseLearned();
}

// 检测本节课是否学过
function checkCourseLearned() {
console.log('checkCourseLearned调用');
// 查找所有具有指定类名的元素
const items = document.querySelectorAll('.resource-item.resource-item-train.resource-item-active');

// 遍历这些元素
items.forEach(item => {
// 查找 <i> 标签
const iElement = item.querySelector('i');

// 检查 <i> 标签的 title 属性是否为 "已学完"
if (iElement && iElement.title === '已学完') {
console.log('找到一个已学完的资源项:', item);
// 如果这节课学完了,就播放下一节课
playNextVideo();
} else {
//如果这节课没学完,监听视频播放结束事件
listenVideoEnd();
}
});
}

// 监听视频播放结束事件
function listenVideoEnd() {
const currentVideo = document.querySelector('video');
console.log('listenVideoEnd调用');
// 监听视频播放结束事件
currentVideo.addEventListener('ended', function () {
console.log('当前视频播放结束,正在尝试播放下一个视频...');
playNextVideo();
});
}

// 播放下一个视频
function playNextVideo() {
console.log('playNextVideo调用');
// 找到当前激活的资源项
const activeItem = document.querySelector('.resource-item.resource-item-train.resource-item-active');
if (!activeItem) {
console.log('没有找到当前激活的资源项');
return;
} else {
console.log(activeItem)
}

// 找到下一个资源项
let nextItem = activeItem.nextElementSibling;
while (nextItem && (!nextItem.classList.contains('resource-item') || !nextItem.classList.contains('resource-item-train'))) {
nextItem = nextItem.nextElementSibling;
}
if (!nextItem) {
// 如果已经是最后一个,循环回到第一个
nextItem = activeItem.parentNode.querySelector('.resource-item.resource-item-train');
}

if (nextItem) {
// 模拟点击下一个资源项
simulateClick(nextItem, function () {
// 延时处理alert并播放视频
setTimeout(handleAlertAndPlayVideo, 3000);
});
} else {
console.log('没有找到下一个资源项或资源项不符合条件');
}

hasVideo();
}

// 模拟点击
function simulateClick(element, callback) {
const clickEvent = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
element.dispatchEvent(clickEvent);
// 如果需要执行回调函数,确保点击事件处理完毕后执行
setTimeout(callback, 0);
}

// 处理alert并播放视频
function handleAlertAndPlayVideo() {
// 这里假设alert是点击后弹出的,并且需要用户确认
if (window.alert) {
// 模拟点击alert的确定按钮
window.alert = function (message) {
console.log('模拟点击alert确定按钮');
};
}
}

// 判断视频播放时长是否等于总时长
function isVideoDurationEqual() {
const video = document.querySelector('video');
if (video) {
const currentTime = video.currentTime;
const duration = video.duration;
const tolerance = 1; // 允许的误差范围,单位为秒

if (Math.abs(currentTime - duration) <= tolerance) {
console.log('视频播放时长等于总时长');
setTimeout(() => {
// 点击video元素,触发视频暂停
simulateClickOnVideo();
}, 3000);
}
}
}
})();

使用

安装篡改猴插件

image-20240802001948601

打开管理面板

image-20240802002031717

添加脚本

image-20240802002101858

将代码扔进去,ctrl + s保存

image-20240802002125428

image-20240802002214549

启用

image-20240802002521991

脚本成功运行

Bug

其实是懒得写还没实现的功能

比如:当最后一个课程学完后,需要打开下一个折叠的层

image-20240802003855298image-20240802003912394

才能自动播放下一个折叠层中的视频

所以播放完一个视频应该先遍历一下所有.resource-item.resource-item-train的类中的所有<i>标签的title是否都为“已学完”,然后再找到折叠层的类,使用浏览器模拟点击让他折叠打开,然后再点击课程…………因为这里没必要所以没写

折叠层的类:image-20240802004238259

判断点击这里就可以实现自动展开了