Archived
为什么:单文件 1100~1900 行的巨石模块让「改一处要通读全文」;同时注释里残留 大量已删台账的编号,以及「Flask/FastAPI 双栈迁移期」这类已失实的架构描述, 会误导后来者按不复存在的架构去改代码。 拆分(对外导入路径不变,行为零变更): - music_downloader.py -> music_downloader/(models/downloader/cover/_http/_log/_concurrency) - auth_system.py -> auth_system/(db/security/users/credits/ledger/mail/cards/backup...) - music_api.py -> music_api/(api/client/crypto/qr/cookies/compat/constants/errors) - services/runtime.py -> services/(api_service/_shared/_taskcore/downloads/batch/podcast/ streaming/tasks);services/runtime.py 保留为兼容门面 兼容性做法:各包用 __init__.py 以 __all__ 原样再导出全部符号,既有调用点 (auth_system.xxx / services.xxx / from music_api import Y)零改动;路径常量改为经包属性 动态读取(auth_system.DB_PATH),否则测试的 monkeypatch 会静默失效并写到生产库。 修复 9 处代码损伤(上一轮注释清理把 `()` 当「空括号残留」删掉所致;用「与拆分前逐字 等价」校验逐一定位,静态检查只会报出其中 4 处): - core/config.py ).strip() or '*' - core/state.py threading.Lock() x2 - qr_login.py datetime.now() / .strip() - music_downloader/downloader.py .lower() - services/podcast.py set() / time.time() x2 清理与删除: - 删除 部署文档.md / 功能介绍.md / README_私有包警告.txt - 清理指向已删台账与文档的过时编号注释;把「迁移期 / 两栈 / 双框架 / 已迁移」等 失实描述改写为现状描述(保留 `<-> Flask /path` 映射表等仍有价值的契约来源说明) - _mkpkg.py:公开包剔除清单去掉已不存在的文档名;私有包警告改为自包含 - pyproject.toml:补 pythonpath = ["."],使裸跑 pytest 不再 import core 失败 验证:ruff 全通过;pyright 0 errors / 0 warnings(102 文件);pytest 144 passed; 真实 uvicorn 冒烟 6 条路由符合预期且生产 data/ 指纹未变;忽略注释的逐字等价性校验通过。
182 lines
6.3 KiB
Python
182 lines
6.3 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""扫码登录管理器。
|
||
|
||
(自 music_api.py 拆出,行为与原实现一致。)
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
from random import randrange
|
||
|
||
from .client import (
|
||
HTTPClient,
|
||
_ensure_dict,
|
||
)
|
||
from .constants import (
|
||
APIConstants,
|
||
)
|
||
from .crypto import (
|
||
CryptoUtils,
|
||
)
|
||
from .errors import (
|
||
APIException,
|
||
)
|
||
|
||
|
||
class QRLoginManager:
|
||
"""二维码登录管理器"""
|
||
|
||
def __init__(self):
|
||
self.http_client = HTTPClient()
|
||
self.crypto_utils = CryptoUtils()
|
||
|
||
async def generate_qr_key(self) -> str | None:
|
||
"""生成二维码的key
|
||
|
||
Returns:
|
||
成功返回unikey,失败返回None
|
||
|
||
Raises:
|
||
APIException: API调用失败时抛出
|
||
"""
|
||
try:
|
||
config = APIConstants.DEFAULT_CONFIG.copy()
|
||
config["requestId"] = str(randrange(20000000, 30000000))
|
||
|
||
payload = {
|
||
'type': 1,
|
||
'header': json.dumps(config)
|
||
}
|
||
|
||
params = self.crypto_utils.encrypt_params(APIConstants.QR_UNIKEY_API, payload)
|
||
response = await self.http_client.post_request_full(APIConstants.QR_UNIKEY_API, params, {})
|
||
|
||
result = json.loads(response.text)
|
||
# 非 dict 响应(null/数组)不再 AttributeError 逃出捕获
|
||
_ensure_dict(result, '生成二维码key响应异常(非标准 JSON 对象)')
|
||
if result.get('code') == 200:
|
||
return result.get('unikey')
|
||
else:
|
||
raise APIException(f"生成二维码key失败: {result.get('message', '未知错误')}")
|
||
except (json.JSONDecodeError, KeyError) as e:
|
||
raise APIException(f"解析二维码key响应失败: {e}") from e
|
||
|
||
async def create_qr_login(self) -> str | None:
|
||
"""创建登录二维码并在控制台显示
|
||
|
||
Returns:
|
||
成功返回unikey,失败返回None
|
||
"""
|
||
try:
|
||
import qrcode
|
||
|
||
unikey = await self.generate_qr_key()
|
||
if not unikey:
|
||
print("生成二维码key失败")
|
||
return None
|
||
|
||
# 创建二维码
|
||
qr = qrcode.QRCode()
|
||
qr.add_data(f'https://music.163.com/login?codekey={unikey}')
|
||
qr.make(fit=True)
|
||
|
||
# 在控制台显示二维码
|
||
qr.print_ascii(tty=True)
|
||
print("\n请使用网易云音乐APP扫描上方二维码登录")
|
||
return unikey
|
||
except ImportError:
|
||
print("请安装qrcode库: pip install qrcode")
|
||
return None
|
||
except Exception as e:
|
||
print(f"创建二维码失败: {e}")
|
||
return None
|
||
|
||
async def check_qr_login(self, unikey: str) -> tuple[int, dict[str, str]]:
|
||
"""检查二维码登录状态
|
||
|
||
Args:
|
||
unikey: 二维码key
|
||
|
||
Returns:
|
||
(登录状态码, cookie字典)
|
||
|
||
Raises:
|
||
APIException: API调用失败时抛出
|
||
"""
|
||
try:
|
||
config = APIConstants.DEFAULT_CONFIG.copy()
|
||
config["requestId"] = str(randrange(20000000, 30000000))
|
||
|
||
payload = {
|
||
'key': unikey,
|
||
'type': 1,
|
||
'header': json.dumps(config)
|
||
}
|
||
|
||
params = self.crypto_utils.encrypt_params(APIConstants.QR_LOGIN_API, payload)
|
||
response = await self.http_client.post_request_full(APIConstants.QR_LOGIN_API, params, {})
|
||
|
||
result = json.loads(response.text)
|
||
# 非 dict 响应不再 AttributeError 逃出捕获(同 generate_qr_key)
|
||
_ensure_dict(result, '解析登录状态响应失败(非标准 JSON 对象)')
|
||
cookie_dict = {}
|
||
|
||
if result.get('code') == 803:
|
||
# 登录成功,提取cookie。
|
||
# 此前只提取 MUSIC_U——__csrf 从未入 Cookie 串,
|
||
# weapi 写接口(发评论等)的 csrf_token 恒为空。改用 requests 的
|
||
# cookie jar(requests 已把多条 Set-Cookie 合并解析,不受逗号合并
|
||
# 的脆弱 split 影响)补齐 MUSIC_U 与 __csrf。
|
||
for name in ('MUSIC_U', '__csrf'):
|
||
val = response.cookies.get(name)
|
||
if val:
|
||
cookie_dict[name] = val
|
||
if not cookie_dict.get('MUSIC_U'):
|
||
# 兜底:jar 异常为空时回退 Set-Cookie 头解析 MUSIC_U
|
||
for cookie_str in response.headers.get('Set-Cookie', '').split(', '):
|
||
if 'MUSIC_U=' in cookie_str:
|
||
cookie_dict['MUSIC_U'] = cookie_str.split('MUSIC_U=')[1].split(';')[0]
|
||
break
|
||
|
||
return result.get('code', -1), cookie_dict
|
||
except (json.JSONDecodeError, KeyError) as e:
|
||
raise APIException(f"解析登录状态响应失败: {e}") from e
|
||
|
||
async def qr_login(self) -> str | None:
|
||
"""完整的二维码登录流程
|
||
|
||
Returns:
|
||
成功返回cookie字符串,失败返回None
|
||
"""
|
||
try:
|
||
unikey = await self.create_qr_login()
|
||
if not unikey:
|
||
return None
|
||
|
||
while True:
|
||
code, cookies = await self.check_qr_login(unikey)
|
||
|
||
if code == 803:
|
||
print("\n登录成功!")
|
||
# CLI 序列化须与 qr_login 口径一致,带上 __csrf
|
||
# (此串正是写往全局 cookie.txt 的内容)
|
||
serialized = f"MUSIC_U={cookies['MUSIC_U']};os=pc;appver=8.9.70;"
|
||
if cookies.get('__csrf'):
|
||
serialized += f"__csrf={cookies['__csrf']};"
|
||
return serialized
|
||
elif code == 801:
|
||
print("\r等待扫码...", end='')
|
||
elif code == 802:
|
||
print("\r扫码成功,请在手机上确认登录...", end='')
|
||
else:
|
||
print(f"\n登录失败,错误码:{code}")
|
||
return None
|
||
|
||
await asyncio.sleep(2)
|
||
except KeyboardInterrupt:
|
||
print("\n用户取消登录")
|
||
return None
|
||
except Exception as e:
|
||
print(f"\n登录过程中发生错误: {e}")
|
||
return None
|