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,117 @@
|
||||
"""网易云个人歌单(移植自 sdk/wy/userPlaylist.ts,上游 music-lib/netease/user_playlist.go)。
|
||||
|
||||
需登录 cookie MUSIC_U(app/sdk/auth_store.py 的 get_credentials('wy')):
|
||||
未登录直接抛清晰错误,不返回空列表 —— 空列表会被前端当成「用户没有歌单」。
|
||||
1) weapi/nuser/account/get → 当前用户 userId
|
||||
2) weapi/user/playlist → 用户歌单列表(分页 limit/offset)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.sdk.auth_store import get_credentials, get_wy_cookie
|
||||
from app.sdk.common.request import http_fetch
|
||||
from app.sdk.common.utils import format_play_count
|
||||
from app.sdk.platforms.wy.utils import UA_LINUX_CHROME
|
||||
from app.sdk.platforms.wy.utils.crypto import weapi
|
||||
|
||||
PLAYLIST_LINK = "https://music.163.com/#/playlist?id={}"
|
||||
|
||||
|
||||
def playlist_link(playlist_id: str) -> str:
|
||||
return PLAYLIST_LINK.format(playlist_id)
|
||||
|
||||
|
||||
def read_cookie() -> str:
|
||||
"""优先用 musicU 拼 MUSIC_U,其次退回已存 cookie / 通用 cookie。"""
|
||||
cred = get_credentials("wy") or {}
|
||||
music_u = str(cred.get("musicU") or "").strip()
|
||||
if music_u:
|
||||
return f"MUSIC_U={music_u}"
|
||||
return str(cred.get("cookie") or get_wy_cookie()).strip()
|
||||
|
||||
|
||||
async def _weapi_post(url: str, data: dict[str, Any], cookie: str) -> dict[str, Any]:
|
||||
resp = await http_fetch(
|
||||
url,
|
||||
method="post",
|
||||
headers={
|
||||
"User-Agent": UA_LINUX_CHROME,
|
||||
"Referer": "https://music.163.com/",
|
||||
"origin": "https://music.163.com",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Cookie": cookie,
|
||||
},
|
||||
form=weapi(data),
|
||||
timeout=15000,
|
||||
)
|
||||
body = resp.body
|
||||
if resp.status_code != 200 or not isinstance(body, dict):
|
||||
raise RuntimeError("wy 个人歌单接口请求失败")
|
||||
return body
|
||||
|
||||
|
||||
async def get_list(page: int = 1, limit: int = 30) -> dict[str, Any]:
|
||||
"""个人歌单列表。未登录 / 登录态失效抛 RuntimeError,不返回空列表。"""
|
||||
cookie = read_cookie()
|
||||
if not cookie:
|
||||
raise RuntimeError("wy 个人歌单需要登录 cookie(MUSIC_U),请先登录网易云")
|
||||
|
||||
safe_page = max(1, int(page or 1))
|
||||
safe_limit = int(limit or 30)
|
||||
if safe_limit <= 0:
|
||||
safe_limit = 30
|
||||
if safe_limit > 100:
|
||||
safe_limit = 100
|
||||
|
||||
account = await _weapi_post("https://music.163.com/weapi/nuser/account/get", {"csrf_token": ""}, cookie)
|
||||
profile = account.get("profile") or {}
|
||||
if account.get("code") != 200 or not profile.get("userId"):
|
||||
raise RuntimeError(f"wy 登录态无效或已过期 (code {account.get('code', 'unknown')})")
|
||||
uid = profile.get("userId")
|
||||
nickname = profile.get("nickname") or ""
|
||||
|
||||
body = await _weapi_post(
|
||||
"https://music.163.com/weapi/user/playlist",
|
||||
{
|
||||
"uid": uid,
|
||||
"limit": safe_limit,
|
||||
"offset": (safe_page - 1) * safe_limit,
|
||||
"includeVideo": True,
|
||||
"csrf_token": "",
|
||||
},
|
||||
cookie,
|
||||
)
|
||||
if body.get("code") != 200:
|
||||
raise RuntimeError(f"wy 个人歌单获取失败 (code {body.get('code')})")
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for item in body.get("playlist") or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
id_ = str(item.get("id") or "").strip()
|
||||
if not id_:
|
||||
continue
|
||||
play_count = int(item.get("playCount") or 0)
|
||||
creator = str((item.get("creator") or {}).get("nickname") or "").strip() or nickname
|
||||
rows.append(
|
||||
{
|
||||
"id": id_,
|
||||
"name": item.get("name") or "",
|
||||
"cover": item.get("coverImgUrl") or "",
|
||||
"trackCount": int(item.get("trackCount") or 0),
|
||||
"playCount": play_count,
|
||||
"play_count": format_play_count(play_count),
|
||||
"creator": creator,
|
||||
"description": item.get("description") or "",
|
||||
"subscribed": bool(item.get("subscribed")),
|
||||
"link": playlist_link(id_),
|
||||
"source": "wy",
|
||||
}
|
||||
)
|
||||
|
||||
return {"list": rows, "page": safe_page, "limit": safe_limit, "total": len(rows), "source": "wy"}
|
||||
|
||||
|
||||
get_user_playlists = get_list
|
||||
Reference in new issue
Block a user