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,92 @@
|
||||
"""JOOX 歌词(对应 sdk/joox/lyric.ts,上游 joox/lyric.go 移植)。
|
||||
|
||||
主通道:GET https://api.joox.com/web-fcgi-bin/web_lyric?musicid=<id>&country=sg&lang=zh_cn
|
||||
响应形如 ``MusicJsonCallback({"code":0,"lyric":"<base64>"})``,lyric 为 base64 UTF-8 歌词。
|
||||
实测(2026-10):该旧接口仍可用(同域的 web_get_songinfo 已 404,但 web_lyric 未下线)。
|
||||
兜底:openjoox2 /track 详情的 ``lrc_content``(同为 base64)。
|
||||
解码:base64 → UTF-8 → HTML 实体(& 等)→ ``{ lyric, tlyric: "" }``。
|
||||
|
||||
QRC(``qrc_content``,XML 格式)上游与 TS 均未解析,此处同样不解析,避免引入未验证逻辑。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.common.request import http_fetch
|
||||
from app.sdk.platforms.joox.music_info import build_track_url
|
||||
from app.sdk.platforms.joox.util import (
|
||||
JOOX_TIMEOUT,
|
||||
decode_joox_lyric,
|
||||
encode_uri_component,
|
||||
first_non_empty,
|
||||
joox_headers,
|
||||
normalize_joox_id,
|
||||
)
|
||||
|
||||
LEGACY_LYRIC_URL = "https://api.joox.com/web-fcgi-bin/web_lyric"
|
||||
|
||||
|
||||
def parse_lyric_payload(body: Any) -> dict[str, Any] | None:
|
||||
"""解析 MusicJsonCallback(...) / 裸 JSON 包裹。"""
|
||||
if isinstance(body, dict):
|
||||
return body
|
||||
text = str(body if body is not None else "")
|
||||
index = text.find("MusicJsonCallback(")
|
||||
if index >= 0:
|
||||
text = text[index + len("MusicJsonCallback(") :]
|
||||
text = text.rstrip()
|
||||
if text.endswith(";"):
|
||||
text = text[:-1].rstrip()
|
||||
if text.endswith(")"):
|
||||
text = text[:-1].rstrip()
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except ValueError:
|
||||
return None
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
|
||||
|
||||
def parse_legacy_lyric(status_code: int, body: Any) -> str:
|
||||
"""上游 GetLyrics 的纯逻辑部分:web_lyric 响应体 → 歌词文本。"""
|
||||
if status_code != 200:
|
||||
raise RuntimeError(f"joox 歌词获取失败(HTTP {status_code})")
|
||||
payload = parse_lyric_payload(body)
|
||||
if not payload:
|
||||
raise RuntimeError("joox 歌词解析失败")
|
||||
raw = str(first_non_empty(payload.get("lyric"), ""))
|
||||
if int(payload.get("code") or 0) != 0 or not raw:
|
||||
raise RuntimeError("joox 歌词不存在或为空")
|
||||
return decode_joox_lyric(raw)
|
||||
|
||||
|
||||
async def get_legacy_lyric(song_id: str) -> str:
|
||||
"""上游 GetLyrics 通道(仅旧接口,测试/内部用)。"""
|
||||
url = f"{LEGACY_LYRIC_URL}?musicid={encode_uri_component(song_id)}&country=sg&lang=zh_cn"
|
||||
res = await http_fetch(url, headers=joox_headers(), timeout=JOOX_TIMEOUT)
|
||||
return parse_legacy_lyric(res.status_code, res.body)
|
||||
|
||||
|
||||
async def get_lyric(song_info: dict[str, Any], *_rest: Any) -> dict[str, str]:
|
||||
"""歌词:旧接口优先,失败回退 openjoox2 详情的 lrc_content。"""
|
||||
song_id = normalize_joox_id(song_info.get("songmid") or song_info.get("id") or song_info.get("songid") or "")
|
||||
if not song_id:
|
||||
raise RuntimeError("joox 歌曲 id 无效")
|
||||
|
||||
# 1) 上游 web_lyric 通道
|
||||
legacy_err: Exception | None = None
|
||||
try:
|
||||
url = f"{LEGACY_LYRIC_URL}?musicid={encode_uri_component(song_id)}&country=sg&lang=zh_cn"
|
||||
res = await http_fetch(url, headers=joox_headers(), timeout=JOOX_TIMEOUT)
|
||||
return {"lyric": parse_legacy_lyric(res.status_code, res.body), "tlyric": ""}
|
||||
except Exception as err: # noqa: BLE001 - 降级链:任一失败都转 openjoox2 兜底
|
||||
legacy_err = err
|
||||
|
||||
# 2) 兜底:openjoox2 详情 lrc_content(base64)
|
||||
res = await http_fetch(build_track_url(song_id), headers=joox_headers(), timeout=JOOX_TIMEOUT)
|
||||
body = res.body if isinstance(res.body, dict) else {}
|
||||
raw = str(first_non_empty(body.get("lrc_content"), ""))
|
||||
if res.status_code != 200 or not raw:
|
||||
raise legacy_err or RuntimeError("joox 歌词不存在或为空")
|
||||
return {"lyric": decode_joox_lyric(raw), "tlyric": ""}
|
||||
Reference in new issue
Block a user