Skip to content

Commit

Permalink
协程bug修复
Browse files Browse the repository at this point in the history
  • Loading branch information
CXRunfree committed Oct 11, 2024
1 parent b20e615 commit 4b611f8
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 156 deletions.
139 changes: 82 additions & 57 deletions Autovisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from playwright.async_api import TimeoutError
from playwright._impl._errors import TargetClosedError
from res.support import show_donate
from res.utils import optimize_page, get_lesson_name, video_optimize, get_filtered_class
from res.utils import optimize_page, get_lesson_name, get_filtered_class, get_video_attr

# 获取全局事件循环
event_loop_verify = asyncio.Event()
Expand All @@ -28,10 +28,10 @@ async def auto_login(config: Config, page: Page):
async def init_page(p: Playwright, config: Config) -> Tuple[Page, Browser]:
driver = "msedge" if config.driver == "edge" else config.driver
if not config.exe_path:
print(f"正在启动{config.driver}浏览器...")
print(f"[Info]正在启动{config.driver}浏览器...")
browser = await p.chromium.launch(channel=driver, headless=False)
else:
print(f"正在启动{config.driver}浏览器...")
print(f"[Info]正在启动{config.driver}浏览器...")
browser = await p.chromium.launch(executable_path=config.exe_path, channel=driver, headless=False)

context = await browser.new_context()
Expand All @@ -47,70 +47,81 @@ async def init_page(p: Playwright, config: Config) -> Tuple[Page, Browser]:
return page, browser


async def tail_work(page: Page, start_time, all_class, title) -> bool:
reachTimeLimit = False
page.set_default_timeout(24 * 3600 * 1000)
time_period = (time.time() - start_time) / 60
if 0 < config.limitMaxTime <= time_period:
print(f"\n当前课程已达时限:{config.limitMaxTime}min\n即将进入下门课程!")
reachTimeLimit = True
else:
class_name = await all_class[-1].get_attribute('class')
if "current_play" in class_name:
print("\n已学完本课程全部内容!")
print("==" * 10)
else:
print(f"\"{title}\" Done !")
print(f"本次课程已学习:{time_period:.1f} min")
return reachTimeLimit
async def video_optimize(page: Page, config: Config) -> None:
await page.wait_for_load_state("domcontentloaded")
while True:
try:
await asyncio.sleep(1)
await page.wait_for_selector("video", state="attached", timeout=3000)
if await get_video_attr(page, "volume") != 0:
await page.evaluate(config.volume_none)
await page.evaluate(config.set_none_icon)
print("[Info]视频已静音.")
if await get_video_attr(page, "playbackRate") != config.limitSpeed:
await page.evaluate(config.revise_speed)
await page.evaluate(config.revise_speed_name)
print(f"[Info]视频已修改为{config.limitSpeed}倍速.")
except Exception as e:
continue


async def play_video(page: Page) -> None:
await page.wait_for_load_state("domcontentloaded")
while True:
try:
await asyncio.sleep(0.5)
playing = await page.query_selector(".pauseButton")
if not playing:
print("[Info]检测到被暂停,正在尝试播放.", end="\r")
canvas = await page.wait_for_selector(".videoArea", state="attached")
await canvas.click()
except TimeoutError:
await asyncio.sleep(1)
await page.wait_for_selector("video", state="attached", timeout=1000)
paused = await page.evaluate("document.querySelector('video').paused")
if paused:
print("\n[Info]检测到视频暂停,正在尝试播放...")
await page.wait_for_selector(".videoArea", timeout=1000)
await page.evaluate('document.querySelector("video").play();')
print("[Info]视频已恢复播放.")
except Exception as e:
continue


async def skip_questions(page: Page, event_loop) -> None:
await page.wait_for_load_state("domcontentloaded")
while True:
try:
await asyncio.sleep(0.5)
await page.wait_for_selector(".topic-item", state="attached", timeout=1000)
if not await page.query_selector(".answer"):
choices = await page.locator(".topic-item").all()
for each in choices:
await each.click()
await page.wait_for_timeout(200)
await page.evaluate(config.close_ques)
await page.wait_for_selector(".el-dialog", state="attached", timeout=1000)
total_ques = await page.query_selector_all(".number")
for ques in total_ques:
await ques.click(timeout=500)
if not await page.query_selector(".answer"):
choices = await page.query_selector_all(".topic-item")
for each in choices[:2]:
await each.click(timeout=500)
await page.wait_for_timeout(100)
await page.press(".el-dialog", "Escape", timeout=1000)
event_loop.set()
except TimeoutError:
except Exception as e:
not_finish_close = await page.query_selector(".el-message-box__headerbtn")
if not_finish_close:
await not_finish_close.click()
continue


async def wait_for_verify(page: Page, event_loop) -> None:
await page.wait_for_load_state("domcontentloaded")
while True:
try:
await asyncio.sleep(1)
await page.wait_for_selector(".yidun_modal__title", state="attached", timeout=1000)
print("\n检测到安全验证,请手动点击完成...")
print("\n[Warn]检测到安全验证,请手动点击完成...")
await page.wait_for_selector(".yidun_modal__title", state="hidden", timeout=24 * 3600 * 1000)
event_loop.set()
print("\n安全验证已完成,继续播放...")
except TimeoutError:
print("\n[Info]安全验证已完成,继续播放...")
except Exception as e:
continue


async def learning_loop(page: Page, config: Config):
title_selector = await page.wait_for_selector(".source-name")
course_title = await title_selector.text_content()
print(f"当前课程:<<{course_title}>>")
print(f"[Info]当前课程:<<{course_title}>>")
await page.wait_for_selector(".clearfix.video", state="attached")
all_class = await get_filtered_class(page)
start_time = time.time()
Expand All @@ -120,13 +131,8 @@ async def learning_loop(page: Page, config: Config):
await page.wait_for_selector(".current_play", state="attached")
await page.wait_for_timeout(1000)
title = await get_lesson_name(page)
print("正在学习:%s" % title)
print("[Info]正在学习:%s" % title)
page.set_default_timeout(10000)
try:
await video_optimize(page, config)
except TimeoutError:
if await page.query_selector(".yidun_modal__title"):
await event_loop_verify.wait()
curtime, total_time = await get_progress(page)
timer = 0
while curtime != "100%":
Expand All @@ -137,7 +143,8 @@ async def learning_loop(page: Page, config: Config):
break
elif timer % 5 == 0:
curtime, total_time = await get_progress(page)
show_progress(desc="完成进度:", cur_str=curtime)
show_progress(desc="完成进度:", cur_time=curtime)
await asyncio.sleep(0.5)
except TimeoutError as e:
if await page.query_selector(".yidun_modal__title"):
await event_loop_verify.wait()
Expand All @@ -153,6 +160,7 @@ async def learning_loop(page: Page, config: Config):


async def reviewing_loop(page: Page, config: Config):
limit_time = config.limitMaxTime
title_selector = await page.wait_for_selector(".source-name")
course_title = await title_selector.text_content()
print(f"当前课程:<<{course_title}>>")
Expand All @@ -167,11 +175,6 @@ async def reviewing_loop(page: Page, config: Config):
title = await get_lesson_name(page)
print("\n正在学习:%s" % title)
page.set_default_timeout(10000)
try:
await video_optimize(page, config)
except TimeoutError:
if await page.query_selector(".yidun_modal__title"):
await event_loop_verify.wait()
curtime, total_time = await get_progress(page)
start_time = time.time()
timer = 0
Expand All @@ -185,8 +188,8 @@ async def reviewing_loop(page: Page, config: Config):
if 0 < config.limitMaxTime <= time_period:
break
elif timer % 5 == 0:
curtime, total_time = await get_progress(page)
show_progress(desc="完成进度:", cur_str=curtime)
show_progress(desc="完成进度:", cur_time=time_period, limit_time=limit_time, enableRepeat=True)
await asyncio.sleep(0.5)
except TimeoutError as e:
if await page.query_selector(".yidun_modal__title"):
await event_loop_verify.wait()
Expand All @@ -200,6 +203,23 @@ async def reviewing_loop(page: Page, config: Config):
if reachTimeLimit:
return

async def tail_work(page: Page, start_time, all_class, title) -> bool:
reachTimeLimit = False
page.set_default_timeout(24 * 3600 * 1000)
time_period = (time.time() - start_time) / 60
if 0 < config.limitMaxTime <= time_period:
print(f"\n当前课程已达时限:{config.limitMaxTime}min\n即将进入下门课程!")
reachTimeLimit = True
else:
class_name = await all_class[-1].get_attribute('class')
if "current_play" in class_name:
print("\n已学完本课程全部内容!")
print("==" * 10)
else:
print(f"\"{title}\" Done !")
print(f"本次课程已学习:{time_period:.1f} min")
return reachTimeLimit


async def entrance(config: Config):
tasks = []
Expand All @@ -208,17 +228,18 @@ async def entrance(config: Config):
page, browser = await init_page(p, config)
# 进行登录
if not config.username or not config.password:
print("请手动输入账号密码...")
print("等待登录完成...")
print("[Info]请手动输入账号密码...")
print("[Info]等待登录完成...")
await auto_login(config, page)
# 启动协程任务
video_optimize_task = asyncio.create_task(video_optimize(page, config))
skip_ques_task = asyncio.create_task(skip_questions(page, event_loop_answer))
play_video_task = asyncio.create_task(play_video(page))
verify_task = asyncio.create_task(wait_for_verify(page, event_loop_verify))
tasks.extend([skip_ques_task, play_video_task, verify_task])
tasks.extend([video_optimize_task, skip_ques_task, play_video_task, verify_task])
# 遍历所有课程,加载网页
for course_url in config.course_urls:
print("开始加载播放页...")
print("[Info]开始加载播放页...")
await page.goto(course_url)
await page.wait_for_selector(".studytime-div")
# 关闭弹窗,优化页面体验
Expand All @@ -245,8 +266,12 @@ async def entrance(config: Config):
print("Github:CXRunfree All Rights Reserved.")
print("===== Runtime Log =====")
try:
print("正在载入数据...")
print("[Info]正在载入数据...")
config = Config()
if not config.course_urls:
print("[Error]不存在有效网址或程序不支持此类网页,请检查配置!")
time.sleep(3)
exit(-1)
asyncio.run(entrance(config))
except Exception as e:
if isinstance(e, KeyError):
Expand Down
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@

------

#### 2024/9/22 fixed-3.14.2 更新

- 修复了进入课程**界面异常**退出的bug;
- 修复了正常结束时的报错问题;
- 修复了旧版本存在的播放完成后多跳过一集的问题;
- 此次更新将程序重构为**异步架构**,使用**协程**处理视频播放、弹题检测和安全验证;
- 避免了主线程阻塞造成的卡死问题,提高了程序的响应性;
- 显著提升弹窗检测频率,减少了因弹窗导致程序卡死的情况。
#### 2024/10/11 stable-3.14.3 更新

重构后的3.14.1~3.14.2版本存在**无法自动播放、答题**的问题,经过测试发现是主线程与协程之间争夺资源导致的;现通过给主线程添加异步休眠已经修复此bug.

**此次更新:**

- 修复了此前出现的无法播放、答题的bug(第N+1次了QwQ)
- 能够跳过存在多页题目的弹题 (之前没遇过这种情况,所以没加)
- 更流畅的程序检测与响应;
- 调整了"复习模式"的进度条样式和日志打印格式;
- 修复了作者懒得改bug的bug.

如若此版本存在稳定性bug,请及时提出issue通知我 ~

------

Expand Down
6 changes: 3 additions & 3 deletions configs.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[user-account]
;配置账号密码,留空则打开网页后需手动输入
username =
password =
username =
password =
[custom-option]
;配置浏览器,可选Edge/Chrome
driver = Edge
Expand All @@ -11,7 +11,7 @@ EXE_PATH =
;允许重复刷已完成的课程(默认:False)
enableRepeat = False
;限制每门课程刷课时长/min (不限制:0)
limitMaxTime = 0
limitMaxTime = 30
;限定播放倍速 (最高:1.8)
limitSpeed = 1.8
[course-url]
Expand Down
2 changes: 1 addition & 1 deletion res/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def get_course_urls(self) -> list:
course_url = self._config.get("course-url", _option, raw=True)
matched = re.findall(self.course_match_rule, course_url)
if not matched:
print(f"\"{course_url.strip()}\"\n不是一个有效网址,将忽略该网址.")
print(f"\"{course_url.strip()}\"\n[Warn]不是一个有效网址,将忽略该网址.")
continue
course_urls.append(course_url)
return course_urls
Expand Down
25 changes: 17 additions & 8 deletions res/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ def parse_time(h, m, s):
return curtime, total_time


def show_progress(desc, cur_str=None, enableRepeat=False):
percent = int(cur_str.split("%")[0]) + 1 # Handles a 1% rendering error
if percent >= 80 and not enableRepeat: # In learning mode, 80% progress is considered complete
percent = 100
length = int(percent * 30 // 100)
progress = ("█" * length).ljust(30, " ")
percent_str = str(percent) + "%"
print(f"\r{desc} |{progress}| {percent_str}\t", end="", flush=True)
def show_progress(desc, cur_time=None, limit_time=None, enableRepeat=False):
if not enableRepeat:
percent = int(cur_time.split("%")[0]) + 1 # Handles a 1% rendering error
if percent >= 80: # In learning mode, 80% progress is considered complete
percent = 100
length = int(percent * 30 // 100)
progress = ("█" * length).ljust(30, " ")
print(f"\r{desc} |{progress}| {percent}%\t", end="", flush=True)
else:
left_time = round(limit_time-cur_time,1)
percent = int(cur_time/limit_time * 100)
if left_time == 0:
percent = 100
length = int(percent * 20 // 100)
progress = ("█" * length).ljust(20, " ")
print(f"\r{desc} |{progress}| {percent}%\t剩余 {left_time} min\t", end="", flush=True)

28 changes: 13 additions & 15 deletions res/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
from playwright.async_api import Page, Locator
from playwright.async_api import TimeoutError
from res.configs import Config
from res.progress import move_mouse
import time


async def optimize_page(page: Page, config: Config) -> None:
try:
await page.wait_for_load_state("domcontentloaded")
await page.evaluate(config.pop_js)
hour = time.localtime().tm_hour
if hour >= 18 or hour < 7:
Expand All @@ -19,26 +19,24 @@ async def optimize_page(page: Page, config: Config) -> None:
await page.evaluate(config.gzh_pop)
await page.wait_for_selector(".warn-box", timeout=1500)
await page.evaluate(config.close_gjh)
except TimeoutError:
except Exception as e:
return


async def get_video_attr(page, attr: str) -> any:
try:
await page.wait_for_selector("video", state="attached", timeout=1000)
attr = await page.evaluate(f'''document.querySelector('video').{attr}''')
return attr
except Exception as e:
return None


async def get_lesson_name(page: Page) -> str:
title_ele = await page.wait_for_selector("#lessonOrder")
await page.wait_for_timeout(500)
title_ = await title_ele.get_attribute("title")
return title_


async def video_optimize(page: Page, config: Config) -> None:
try:
await move_mouse(page)
await page.evaluate(config.volume_none)
await page.evaluate(config.set_none_icon)
await page.evaluate(config.revise_speed)
await page.evaluate(config.revise_speed_name)
except Exception as e:
print(f"\n[Warn]{repr(e)}")
title = await title_ele.get_attribute("title")
return title


async def get_filtered_class(page: Page, enableRepeat=False) -> List[Locator]:
Expand Down
Loading

0 comments on commit 4b611f8

Please sign in to comment.