-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
217 lines (188 loc) · 8.78 KB
/
main.py
File metadata and controls
217 lines (188 loc) · 8.78 KB
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
216
217
"""
主入口文件 - 使用模块化架构
配置请在 config.py 中修改
"""
import asyncio
import sys
import warnings
from getpass import getpass
from pathlib import Path
import config
from automation import AuthManager, BrowserManager, VideoManager
from automation.exception_context import BrowserClosedError
from cookie_fix import cookie_fix
from logger import get_logger, setup_logging
# 抑制 asyncio 在 Windows 上关闭时的资源警告
warnings.filterwarnings("ignore", category=ResourceWarning, message=".*unclosed.*")
def _custom_unraisablehook(unraisable):
"""自定义 unraisable 异常处理,抑制浏览器关闭时的 asyncio 清理错误"""
# 忽略 asyncio transport 相关的清理错误
if unraisable.exc_type in (ValueError, OSError):
err_msg = str(unraisable.exc_value).lower()
if any(
keyword in err_msg
for keyword in [
"i/o operation on closed pipe",
"closed pipe",
"unclosed transport",
]
):
return # 静默忽略
# 其他异常使用默认处理
sys.__unraisablehook__(unraisable)
def _custom_excepthook(exc_type, exc_value, exc_traceback):
"""全局异常处理器,将未捕获的异常记录到日志文件"""
# KeyboardInterrupt 不记录日志,只做友好提示
if issubclass(exc_type, KeyboardInterrupt):
return
# 记录完整的异常信息到日志
logger.critical("未捕获的异常", exc_info=(exc_type, exc_value, exc_traceback))
sys.unraisablehook = _custom_unraisablehook
sys.excepthook = _custom_excepthook
logger = get_logger(__name__)
def print_welcome():
"""打印欢迎界面"""
welcome_art = """
╔══════════════════════════════════════════════════════════════╗
║ ║
║ Fly Vedio Assignment Away ║
║ ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ 欢迎使用 FlyVedioAssignmentAway ║
║ 使用说明: github.com/YewFence/fly_vedio_assignment_away ║
║ 作者: YewFence ║
║ ║
╚══════════════════════════════════════════════════════════════╝
"""
print(welcome_art)
print("🚀 程序启动中...\n")
print("💡 提示: 可按下 Ctrl+C 结束程序\n")
async def main():
"""主函数"""
# 初始化日志系统
setup_logging()
# 显示欢迎界面
print_welcome()
# 从 config.py 读取配置
logger.info("📦 正在初始化浏览器...")
browser_manager = None
try:
# 1. 启动浏览器
browser_manager = BrowserManager(
browser_type=config.BROWSER, headless=config.HEADLESS
)
await browser_manager.setup()
# 2. 初始化认证和视频管理器
page = browser_manager.get_page()
context = browser_manager.get_context()
auth_manager = AuthManager(page, context)
video_manager = VideoManager(page, auth_manager)
login_success = False
# 测试模式下跳过尝试,进行登录凭证获取测试
if not config.TEST_LOGIN_MODE:
cookie_path = Path(config.COOKIE_FILE)
# 如果 cookies.json 文件已存在,尝试直接使用已有 Cookies 登录
if cookie_path.exists():
logger.info(
f"📂 检测到已有 Cookie 文件: {config.COOKIE_FILE},尝试直接使用该文件登录..."
)
login_success = await auth_manager.login_with_cookies(
config.BASE_URL, config.COOKIE_FILE
)
if not login_success:
logger.warning("登录凭证已失效或不存在")
# 选择登录方式 - 保留 print 用于用户交互
print("\n🔐 请选择获取登录凭证(Cookies)的方式:")
print(" 1. 账号密码登录(推荐)- 在命令行输入账号密码,自动完成登录")
print(
" 2. 使用您手动获取的 Cookies 登录 - 在命令行中直接粘贴浏览器导出的 Cookies JSON"
)
login_success = False
while True:
try:
loop = asyncio.get_running_loop()
choice = await loop.run_in_executor(
None, input, "请输入选择 (1/2,默认为1): "
)
choice = choice.strip()
if choice in ("", "1"):
# 账号密码登录,最多允许3次尝试
username = await loop.run_in_executor(
None, input, "请输入账号: "
)
for attempt in range(1, 4):
password = await loop.run_in_executor(
None, getpass, "请输入密码(输入时不会显示): "
)
login_success = await auth_manager.credential_login(
username.strip(),
password,
config.LOGIN_URL,
config.BASE_URL,
config.SSO_INDEX_URL,
config.COOKIE_FILE,
)
if login_success:
break
remaining = 3 - attempt
if remaining > 0:
logger.warning(
f"本次登录未成功,还可重试 {remaining} 次"
)
break
elif choice == "2":
# 使用手动导出的 cookies 登录
if cookie_fix():
logger.info("✓ Cookies 格式化成功")
login_success = await auth_manager.login_with_cookies(
config.BASE_URL, config.COOKIE_FILE
)
else:
logger.error(
"⚠ Cookies 格式化失败,请检查输入的 Cookies 内容是否正确,程序即将结束"
)
break
else:
print("⚠️ 输入无效,请输入 1 或 2")
except KeyboardInterrupt:
logger.info("\n\n程序已由用户中断。")
return
if not login_success:
logger.error("\n❌ 登录失败!")
return
# 4. 通过URL模式获取视频链接
logger.info("\n正在提取视频链接...")
logger.info(f"URL模式: {config.URL_PATTERN}")
video_links = await video_manager.get_video_links_by_pattern(
config.VIDEO_LIST_URL, config.URL_PATTERN
)
# 5. 观看所有视频
if video_links:
await video_manager.watch_videos(
video_links,
config.VIDEO_ELEMENT_SELECTOR,
config.PLAY_BUTTON_SELECTOR,
config.DEFAULT_WAIT_TIME,
)
else:
logger.error("❌ 未找到任何视频链接。")
suggestions()
except BrowserClosedError:
logger.info("\n👋 检测到浏览器已关闭,程序正常退出")
except Exception:
# 只打印一次:RichHandler 会负责美化堆栈,避免和 traceback.print_exc() 重复输出。
logger.error("\n❌ 发生错误", exc_info=True)
suggestions()
def suggestions():
logger.info("\n💡 故障排查建议:")
logger.info(" 1. 检查 .env 文件中是否正确配置了课程链接")
logger.info(" 2. 确认网络状态良好")
logger.info(
" 3. 如仍有问题,请附上 log/debug.log 文件提交 issue 至 GitHub 仓库:github.com/YewFence/fly_vedio_assignment_away\n"
)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n👋 程序已由用户中断,再见!")