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

+118
View File
@@ -0,0 +1,118 @@
"""5sing 歌曲信息 / 取流底层(对应 sdk/5sing/musicInfo.ts)。
链路(上游 fivesing/fivesing.go 的 fetchSongInfo / fetchAudioLink):
取流: GET {MOBILE_API}/song/getSongUrl?songid=&songtype= → data.{sq,hq,lq}url(_backup),code 必须 1000
详情: GET {MOBILE_API}/song/newget?songid=&songtype= → data.SN / data.user.NN / data.user.I / data.dynamicWords
档位字段:squrl(无损/高码)→ hqurl(高)→ lqurl(低),每档有 _backup 备用地址;
上游固定 sq→hq→lq;本移植按请求档位微调(请求 64k/lq 时倒序),返回实际命中档位。
"""
from __future__ import annotations
from typing import Any
from app.sdk.common.utils import as_dict
from app.sdk.platforms.fivesing.util import (
MOBILE_API,
build_song_link,
encode_uri_component,
estimate_duration,
fetch_5sing_json,
get_first_valid,
kind_name,
)
QUALITY_TIERS = ("sq", "hq", "lq")
def _tier_url(data: dict[str, Any], tier: str) -> str:
"""data 里某档位的可用地址(主地址 → backup)。"""
return get_first_valid(data.get(f"{tier}url"), data.get(f"{tier}url_backup"))
async def fetch_audio_link(song_id: str, song_type: str, prefer_low: bool = False) -> dict[str, str]:
"""上游 fetchAudioLink:仅获取音频链接。"""
url = (
f"{MOBILE_API}/song/getSongUrl?songid={encode_uri_component(song_id)}"
f"&songtype={encode_uri_component(song_type)}"
)
resp = await fetch_5sing_json(url)
body = resp.body if isinstance(resp.body, dict) else {}
if resp.status_code != 200 or not isinstance(resp.body, dict):
raise RuntimeError(f"5sing 取流失败(HTTP {resp.status_code})")
try:
code = int(body.get("code") or 0)
except (TypeError, ValueError):
code = 0
if code != 1000:
# 实测 songtype 缺失/错误时 code=28 message=歌曲不存在
message = body.get("message")
raise RuntimeError(f"5sing 取流失败({message})" if message else "api returned error code")
data = as_dict(body.get("data"))
order = tuple(reversed(QUALITY_TIERS)) if prefer_low else QUALITY_TIERS
for tier in order:
hit = _tier_url(data, tier)
if hit and hit.startswith(("http://", "https://")):
return {"type": tier, "url": hit}
raise RuntimeError("no valid download url found")
async def fetch_new_get(song_id: str, song_type: str) -> dict[str, Any]:
"""上游 fetchSongInfo / GetLyrics 使用的 newget 详情(返回 data 对象)。"""
url = f"{MOBILE_API}/song/newget?songid={encode_uri_component(song_id)}&songtype={encode_uri_component(song_type)}"
resp = await fetch_5sing_json(url)
if resp.status_code != 200 or not isinstance(resp.body, dict):
raise RuntimeError(f"5sing 歌曲信息获取失败(HTTP {resp.status_code})")
data = resp.body.get("data")
return data if isinstance(data, dict) else {}
async def fetch_dynamic_lyric(song_id: str, song_type: str) -> str:
"""上游 newget 歌词:data.dynamicWords(LRC 文本)。"""
data = await fetch_new_get(song_id, song_type)
return str(data.get("dynamicWords") or "")
async def fetch_song_info(song_id: str, song_type: str) -> dict[str, Any]:
"""上游 fetchSongInfo:音频直链 + newget 元数据 → 统一 Song 形状。"""
hit = await fetch_audio_link(song_id, song_type)
name = ""
artist = ""
cover = ""
try:
data = await fetch_new_get(song_id, song_type)
name = str(data.get("SN") or "")
user = as_dict(data.get("user"))
artist = str(user.get("NN") or "")
cover = str(user.get("I") or "")
except Exception: # noqa: BLE001 - 上游 newget 失败时继续(不阻断)
pass
if not name:
name = f"5sing_{song_type}_{song_id}"
return {
"songmid": f"{song_id}|{song_type}",
"name": name,
"singer": artist,
"albumId": "",
"albumName": "",
"interval": 0,
"img": cover or None,
"lrc": None,
"types": [],
"_types": {},
"url": hit["url"],
"type": hit["type"],
"link": build_song_link(song_type, song_id),
"songType": song_type,
"typeName": kind_name(song_type),
"source": "5sing",
}
def song_interval(song_size: Any) -> int:
"""时长估算的对外小工具(搜索结果用;上游 Search 的 Duration 公式)。"""
return estimate_duration(song_size)