Skip to content

Commit d49ee2e

Browse files
QilanLinclaude
andcommitted
修复 Firefox cookies 导出和 Book 章节视频下载问题
## 主要改进 ### 1. 修复 Firefox cookies 数据库问题 - 添加 `_repair_firefox_cookies_db()` 自动修复函数 - 当主文件损坏时自动从 .bak 备份恢复 - 改进错误提示,提供具体解决方案 - 文件: export_browser_cookies.py, cookie_manager.py ### 2. 恢复 Mobile API 章节视频提取功能 - 恢复从 fileurl 下载完整章节 HTML 的逻辑 - Mobile API 的 content 字段只包含标题,完整内容需从 fileurl 下载 - 这样可以从章节 HTML 中正确提取 Kaltura 视频 - 添加详细的调试日志便于诊断 - 文件: moodle_dl/moodle/mods/book.py ### 3. 修复 Playwright cookies 格式转换 - 确保 secure 字段是布尔值(Firefox 可能返回 0 或 1) - 修复 "storageState.cookies[0].secure: expected boolean, got number" 错误 - 文件: moodle_dl/cookie_manager.py ### 4. 添加 SSO 自动登录模块 - 新增 auto_sso_login.py 用于自动化 SSO 登录 - 支持从浏览器读取 SSO cookies 自动完成登录 - 文件: moodle_dl/auto_sso_login.py ## 问题背景 用户反馈 Book 章节中的 Kaltura 视频无法下载,经调查发现: 1. Firefox cookies.sqlite 数据库损坏,无法从备份恢复 2. Mobile API 返回的章节 content 只有标题,完整 HTML 需从 fileurl 下载 3. Playwright cookies 转换中 secure 字段类型不匹配 ## 测试结果 ✅ Firefox cookies 成功恢复和导出 ✅ API token 自动获取成功 ✅ 配置向导正常运行 ✅ 可以从 Book 章节中提取视频(恢复旧功能) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 9b54527 commit d49ee2e

7 files changed

Lines changed: 1105 additions & 296 deletions

File tree

export_browser_cookies.py

Lines changed: 145 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,75 @@ def find_browser_cookie_path(browser_name: str) -> str:
8787
return None
8888

8989

90+
def _repair_firefox_cookies_db():
91+
"""
92+
修复Firefox的cookies.sqlite数据库
93+
94+
如果主文件不存在或损坏,自动从.bak备份恢复
95+
这解决了Firefox关闭后WAL文件无法合并的问题
96+
"""
97+
import platform
98+
import os
99+
100+
system = platform.system()
101+
102+
# 获取Firefox Profile路径
103+
if system == 'Darwin': # macOS
104+
profile_base = os.path.expanduser('~/Library/Application Support/Firefox/Profiles')
105+
elif system == 'Linux':
106+
profile_base = os.path.expanduser('~/.mozilla/firefox')
107+
elif system == 'Windows':
108+
profile_base = os.path.join(os.getenv('APPDATA', ''), 'Mozilla', 'Firefox', 'Profiles')
109+
else:
110+
return False
111+
112+
if not os.path.exists(profile_base):
113+
return False
114+
115+
# 找到default或default-release profile
116+
for profile_dir in os.listdir(profile_base):
117+
if 'default' not in profile_dir.lower():
118+
continue
119+
120+
profile_path = os.path.join(profile_base, profile_dir)
121+
cookies_main = os.path.join(profile_path, 'cookies.sqlite')
122+
cookies_bak = os.path.join(profile_path, 'cookies.sqlite.bak')
123+
cookies_wal = os.path.join(profile_path, 'cookies.sqlite-wal')
124+
125+
# 检查是否需要修复
126+
main_exists = os.path.exists(cookies_main) and os.path.getsize(cookies_main) > 0
127+
bak_exists = os.path.exists(cookies_bak) and os.path.getsize(cookies_bak) > 1024 # > 1KB
128+
129+
if not main_exists and bak_exists:
130+
# 主文件不存在或为空,但有有效的备份
131+
try:
132+
import shutil
133+
shutil.copy(cookies_bak, cookies_main)
134+
135+
# 清理WAL文件(可能为空或损坏)
136+
if os.path.exists(cookies_wal):
137+
os.remove(cookies_wal)
138+
139+
return True
140+
except Exception as e:
141+
continue
142+
143+
return False
144+
145+
90146
def export_cookies_from_browser(domain: str, output_file: str, browser_name='chrome'):
91147
"""从指定浏览器导出 cookies"""
92148

93149
print(f"正在从 {browser_name} 导出 cookies...")
94150

151+
# Firefox特殊处理:尝试修复数据库
152+
if browser_name.lower() == 'firefox':
153+
try:
154+
if _repair_firefox_cookies_db():
155+
print(f" ℹ️ 已自动修复Firefox cookies数据库")
156+
except Exception as e:
157+
pass # 静默处理,如果修复失败就继续用browser_cookie3的默认逻辑
158+
95159
try:
96160
# 定义浏览器到 browser_cookie3 方法的映射
97161
# 使用元组: (方法, 是否需要自定义路径)
@@ -227,7 +291,31 @@ def export_cookies_from_browser(domain: str, output_file: str, browser_name='chr
227291
return True
228292

229293
except Exception as e:
230-
print(f"❌ 导出失败: {e}")
294+
error_msg = str(e)
295+
print(f"❌ 导出失败: {error_msg}")
296+
297+
# 针对Firefox特定错误提供详细的解决方案
298+
if 'Failed to find Firefox cookie file' in error_msg or \
299+
('firefox' in browser_name.lower() and 'cookie' in error_msg.lower()):
300+
print(f"\n💡 Firefox Cookie 导出失败的常见原因与解决方案:")
301+
print(f" 1. Firefox 正在运行(最常见)")
302+
print(f" → 解决:关闭 Firefox 后重试")
303+
print(f" 命令:killall Firefox && sleep 3")
304+
print(f" 2. Firefox cookies.sqlite 数据库损坏")
305+
print(f" → Firefox 会自动创建 cookies.sqlite.bak 备份")
306+
print(f" → moodle-dl 现在可以自动从备份恢复")
307+
print(f" → 重新运行此命令应该可以成功")
308+
print(f" 3. WAL 文件异常")
309+
print(f" → 关闭 Firefox 会清理 WAL 文件")
310+
print(f" → 或手动删除:rm ~/Library/Application\\ Support/Firefox/Profiles/*/cookies.sqlite-wal")
311+
print(f" 4. Firefox 从未运行过或 Profile 损坏")
312+
print(f" → 至少运行一次 Firefox 并访问 {domain}")
313+
print(f"\n⚠️ 推荐步骤:")
314+
print(f" 1. killall Firefox # 关闭 Firefox")
315+
print(f" 2. sleep 3 # 等待系统清理")
316+
print(f" 3. moodle-dl # 重新运行此命令")
317+
print(f"\n 或者选择其他浏览器(Chrome/Edge/Safari)")
318+
231319
return False
232320

233321
def test_cookies(domain: str, cookies_file: str):
@@ -386,128 +474,9 @@ def save_playwright_cookies_to_netscape(playwright_cookies: list, output_file: s
386474
return False
387475

388476

389-
def auto_refresh_session_with_sso(domain: str, cookies_file: str, browser='firefox') -> bool:
390-
"""
391-
利用现有SSO cookies自动刷新Moodle session
392-
393-
核心思路:
394-
1. 从系统浏览器导出现有cookies(可能包含过期MoodleSession + 有效SSO cookies)
395-
2. 使用Playwright加载这些cookies访问Moodle
396-
3. 如果SSO cookies有效,Moodle会自动重定向到SSO→认证→返回,生成新的MoodleSession
397-
4. 提取fresh cookies并保存
398-
399-
Args:
400-
domain: Moodle域名
401-
cookies_file: cookies文件路径(会被更新为fresh cookies)
402-
browser: 浏览器名称(用于导出现有cookies)
403-
404-
Returns:
405-
是否成功刷新
406-
"""
407-
print("\n" + "="*80)
408-
print("🔄 智能Cookie刷新")
409-
print("="*80)
410-
print("正在尝试利用现有SSO cookies自动刷新MoodleSession...")
411-
print("(如果SSO cookies仍有效,可以无需手动登录即可刷新)")
412-
print("="*80 + "\n")
413-
414-
try:
415-
from playwright.sync_api import sync_playwright
416-
import time
417-
418-
# 步骤1: 导出现有cookies(包括可能过期的MoodleSession + SSO cookies)
419-
print("📤 步骤1: 导出现有cookies(包括SSO认证信息)...")
420-
if not export_cookies_from_browser(domain, cookies_file, browser):
421-
print("❌ 无法导出现有cookies")
422-
return False
423-
424-
# 步骤2: 加载cookies到Playwright
425-
print("\n🔄 步骤2: 使用Playwright自动刷新session...")
426-
playwright_cookies = convert_netscape_to_playwright(cookies_file)
427-
428-
if not playwright_cookies:
429-
print("❌ 无法加载cookies")
430-
return False
431-
432-
print(f" ✓ 已加载 {len(playwright_cookies)} 个cookies")
433-
434-
# 步骤3: 访问Moodle触发SSO自动认证
435-
with sync_playwright() as p:
436-
print(" ✓ 启动Firefox浏览器(headless模式)...")
437-
browser_instance = p.firefox.launch(headless=True)
438-
context = browser_instance.new_context()
439-
440-
# 加载cookies
441-
context.add_cookies(playwright_cookies)
442-
443-
page = context.new_page()
444-
445-
try:
446-
moodle_url = f'https://{domain}' if not domain.startswith('http') else domain
447-
448-
print(f" ✓ 访问Moodle主页: {moodle_url}")
449-
start_time = time.time()
450-
451-
# 访问Moodle,会自动触发SSO重定向(如果cookies有效)
452-
page.goto(moodle_url, wait_until='networkidle', timeout=60000)
453-
454-
elapsed = time.time() - start_time
455-
final_url = page.url
456-
457-
print(f" ✓ 页面加载完成(耗时 {elapsed:.1f}秒)")
458-
print(f" ✓ 最终URL: {final_url}")
459-
460-
# 检查是否成功(不在login页面)
461-
if 'login' in final_url.lower() and domain in final_url:
462-
# 仍然在Moodle的login页面 = SSO cookies也过期了
463-
print("\n❌ SSO cookies已过期,需要手动登录")
464-
print(" → Moodle要求重新认证")
465-
browser_instance.close()
466-
return False
467-
468-
# 成功!可能经过了SSO重定向,或者直接进入了Moodle
469-
print("\n✅ SSO自动认证成功!")
470-
471-
# 步骤4: 提取fresh cookies
472-
print("\n📥 步骤3: 提取fresh cookies...")
473-
fresh_cookies = context.cookies()
474-
print(f" ✓ 提取到 {len(fresh_cookies)} 个cookies")
475-
476-
# 检查是否包含新的MoodleSession
477-
moodle_sessions = [c for c in fresh_cookies if 'MoodleSession' in c['name']]
478-
if moodle_sessions:
479-
print(f" ✓ 包含 {len(moodle_sessions)} 个MoodleSession cookie")
480-
for ms in moodle_sessions:
481-
print(f" - {ms['name']} (domain: {ms['domain']})")
482-
483-
# 步骤5: 保存fresh cookies
484-
print("\n💾 步骤4: 保存fresh cookies...")
485-
if save_playwright_cookies_to_netscape(fresh_cookies, cookies_file):
486-
print(f" ✅ Fresh cookies已保存到: {cookies_file}")
487-
browser_instance.close()
488-
489-
print("\n" + "="*80)
490-
print("✅ 自动刷新成功!无需手动登录")
491-
print("="*80 + "\n")
492-
return True
493-
else:
494-
print(" ❌ 保存cookies失败")
495-
browser_instance.close()
496-
return False
497-
498-
except Exception as e:
499-
print(f"\n❌ 访问Moodle时出错: {e}")
500-
browser_instance.close()
501-
return False
502-
503-
except ImportError:
504-
print("❌ 需要安装Playwright: pip install playwright && playwright install firefox")
505-
return False
506-
except Exception as e:
507-
print(f"❌ 自动刷新失败: {e}")
508-
import traceback
509-
traceback.print_exc()
510-
return False
477+
# ⚠️ 已废弃: auto_refresh_session_with_sso()
478+
# 此函数已被 auto_sso_login.py 中的 auto_login_with_sso_sync() 取代
479+
# 为了符合 DRY 原则,不再维护重复的实现
511480

512481

513482
def extract_api_token_with_playwright(domain: str, cookies_file: str):
@@ -1050,22 +1019,60 @@ def export_cookies_interactive(domain: str = None, output_file: str = None, ask_
10501019
print("=" * 80)
10511020
return False
10521021

1053-
# 🔄 智能刷新:尝试使用SSO cookies自动刷新MoodleSession
1054-
# 这样即使导出的MoodleSession过期了,也能自动刷新
1055-
print("\n正在验证cookies并尝试智能刷新...")
1056-
refresh_success = auto_refresh_session_with_sso(domain, output_file, selected_browser)
1022+
# 🚀 使用完全自动化的 SSO 登录来刷新 cookies
1023+
# 只要 SSO cookies 有效,完全无需手动操作
1024+
print("\n🔄 正在使用自动 SSO 登录刷新 cookies...")
1025+
print(" (只要 Microsoft/Google 的 SSO cookies 有效,将完全自动化)")
10571026

1058-
if not refresh_success:
1059-
# 刷新失败,说明SSO cookies也过期了
1060-
print("\n" + "=" * 80)
1061-
print("⚠️ 智能刷新失败 - SSO cookies已过期")
1062-
print("=" * 80)
1063-
print("\n你需要手动登录以刷新cookies:")
1064-
print(f"1. 在{selected_browser}浏览器中访问: https://{domain}")
1065-
print("2. 完成SSO登录(Microsoft/Google等)")
1066-
print("3. 登录成功后,重新运行此命令")
1067-
print("=" * 80)
1068-
return False
1027+
try:
1028+
# 导入自动 SSO 登录模块
1029+
# 注意:这个脚本可能从不同位置被调用,需要动态导入
1030+
import importlib.util
1031+
import sys
1032+
1033+
# 尝试直接导入(如果在 moodle-dl 环境中)
1034+
try:
1035+
from moodle_dl.auto_sso_login import auto_login_with_sso_sync
1036+
except ImportError:
1037+
# 回退:尝试从文件路径加载
1038+
auto_sso_path = os.path.join(
1039+
os.path.dirname(os.path.abspath(__file__)),
1040+
'moodle_dl', 'auto_sso_login.py'
1041+
)
1042+
if os.path.exists(auto_sso_path):
1043+
spec = importlib.util.spec_from_file_location("auto_sso_login", auto_sso_path)
1044+
auto_sso_module = importlib.util.module_from_spec(spec)
1045+
spec.loader.exec_module(auto_sso_module)
1046+
auto_login_with_sso_sync = auto_sso_module.auto_login_with_sso_sync
1047+
else:
1048+
raise ImportError("Cannot find auto_sso_login module")
1049+
1050+
# 使用自动 SSO 登录刷新 cookies
1051+
refresh_success = auto_login_with_sso_sync(
1052+
moodle_domain=domain,
1053+
cookies_path=output_file,
1054+
preferred_browser=selected_browser,
1055+
headless=True # 后台运行
1056+
)
1057+
1058+
if not refresh_success:
1059+
# 自动 SSO 登录失败 - 说明 SSO cookies 完全过期
1060+
print("\n" + "=" * 80)
1061+
print("⚠️ 自动 SSO 登录失败 - SSO cookies 已完全过期")
1062+
print("=" * 80)
1063+
print("\n💡 解决方案:")
1064+
print(f" 在{selected_browser}浏览器中访问 {domain} 并完成 SSO 登录")
1065+
print(f" 登录后,SSO cookies 会自动保存到浏览器")
1066+
print(f" 然后重新运行此命令,将能够完全自动化")
1067+
print("\n📌 这是唯一需要手动操作的场景(SSO cookies 完全过期时)")
1068+
print(" 之后的所有操作都将完全自动化,无需再次手动登录")
1069+
print("=" * 80)
1070+
return False
1071+
1072+
except Exception as e:
1073+
print(f"\n⚠️ 自动 SSO 登录出错: {e}")
1074+
print(" 回退到测试现有 cookies...")
1075+
refresh_success = False
10691076

10701077
# 测试 cookies
10711078
cookies_valid = test_cookies(domain, output_file)

0 commit comments

Comments
 (0)