feat(python): 完成 Python 全量迁移——剩余平台、登录通道与公共层修复

- 新增 5sing/apple/xm/me/qt/lr 平台包与取流实现(含 xm-sign、MissEvan 签名、蜻蜓 HMAC、Apple developer token)
- 新增 wy/kg/tx/lr 登录:扫码、手机号、账密(kg 20028 风控指纹)、cookie 绑定、kg/tx 续期
- request 层:保留多值 Set-Cookie;JSON body 紧凑化(修复 kg 网关验签);直连不经系统代理(对齐 Node)
- 登录路由统一 Body(embed=True),修复扫码轮询 422;lifespan 接入登录续期调度
- ruff/format/pyright 全绿;14 平台真实联网与 HTTP 路由验证通过
This commit is contained in:
snowy-yang committed 2026-10-23 21:55:45 +08:00
1 parent 800871ef6e
commit dfc06bf4de
182 files changed
+26620

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
"""酷狗歌词(对应 sdk/kg/lyric.ts)。
两步:`lyrics.kugou.com/search` 按歌名+hash+时长找候选 → `lyrics.kugou.com/download`
取回 base64 内容。`fmt=krc` 需走 krc_decode 解密,`fmt=lrc` 直接 base64 解码。
"""
from __future__ import annotations
import base64
from typing import Any
from app.sdk.common.request import http_fetch
from app.sdk.platforms.kg.krc_decode import decode_krc
HEADERS = {
"KG-RC": "1",
"KG-THash": "expand_search_manager.cpp:852736169:451",
"User-Agent": "KuGou2012-9020-ExpandSearchManager",
}
def get_intv(interval: Any) -> int:
"""`mm:ss` → 毫秒(原文把秒当毫秒拼给上游,这里保持一致)。"""
if not interval:
return 0
parts = str(interval).split(":")
total = 0
unit = 1
while parts:
part = parts.pop()
try:
total += int(float(part)) * unit
except ValueError:
pass
unit *= 60
return int(total)
async def _search_lyric(name: str, hash_value: str, time: Any, try_num: int = 0) -> dict[str, str] | None:
from app.sdk.platforms.kg.util import encode_uri_component
url = (
"http://lyrics.kugou.com/search?ver=1&man=yes&client=pc"
f"&keyword={encode_uri_component(name)}&hash={hash_value}&timelength={time}&lrctxt=1"
)
resp = await http_fetch(url, headers=HEADERS)
if resp.status_code != 200:
if try_num > 5:
raise RuntimeError("歌词获取失败")
return await _search_lyric(name, hash_value, time, try_num + 1)
body = resp.body if isinstance(resp.body, dict) else {}
candidates = body.get("candidates") or []
if not candidates:
return None
info = candidates[0]
fmt = "krc" if info.get("krctype") == 1 and info.get("contenttype") != 1 else "lrc"
return {"id": str(info.get("id")), "accessKey": str(info.get("accesskey")), "fmt": fmt}
async def _get_lyric_download(id: str, access_key: str, fmt: str, try_num: int = 0) -> dict[str, str]:
url = f"http://lyrics.kugou.com/download?ver=1&client=pc&id={id}&accesskey={access_key}&fmt={fmt}&charset=utf8"
resp = await http_fetch(url, headers=HEADERS)
if resp.status_code != 200:
if try_num > 5:
raise RuntimeError("歌词获取失败")
# 重试必须回到本方法(搜索接口与下载接口的入参语义不同)
return await _get_lyric_download(id, access_key, fmt, try_num + 1)
body = resp.body if isinstance(resp.body, dict) else {}
content = str(body.get("content") or "")
match body.get("fmt"):
case "krc":
return await decode_krc(content)
case "lrc":
return {
"lyric": base64.b64decode(content).decode("utf-8", errors="replace"),
"tlyric": "",
"rlyric": "",
"yrc": "",
}
case _:
raise RuntimeError(f"未知歌词格式: {body.get('fmt')}")
async def get_lyric(song_info: dict[str, Any]) -> dict[str, str]:
name = str(song_info.get("name") or "")
hash_value = str(song_info.get("hash") or song_info.get("songmid") or "")
time = song_info.get("_interval") or get_intv(song_info.get("interval"))
result = await _search_lyric(name, hash_value, time)
if not result:
raise RuntimeError("Get lyric failed")
return await _get_lyric_download(result["id"], result["accessKey"], result["fmt"])