Files
MusicServer/app/sdk/platforms/wy/user_playlist.py
T
snowy-yang dfc06bf4de 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 路由验证通过
2026-10-23 21:55:45 +08:00

118 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""网易云个人歌单(移植自 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