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:
1 parent
800871ef6e
commit
dfc06bf4de
182 files changed
+26620
No files matched your search
@@ -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"])
|
||||
Reference in new issue
Block a user