Files
MusicServer/app/sdk/platforms/wy/link.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

85 lines
2.9 KiB
Python

"""网易云链接解析(移植自 sdk/wy/link.ts,上游 music-lib/netease/netease.go 的 parseNeteaseLink)。
支持:
歌曲 https://music.163.com/#/song?id=29732995 /song?id= / /song/29732995
专辑 https://music.163.com/#/album?id=32311 /album?id= / /album/32311
歌单 https://music.163.com/#/playlist?id=7507256008 /playlist?id= / /playlist/<id>/<n>/...
裸数字 id 由各模块自行兜底(上游 Parse 不接受裸 id)。
顺序上依次尝试原链接、path+query、hash 片段 —— 网页版把真实路径放在 # 后面,
因此必须逐候选尝试,只看 pathname 会漏掉 `#/song?id=` 这类主流形态。
"""
from __future__ import annotations
import re
from typing import Any, Literal
from urllib.parse import parse_qs, urljoin, urlparse
NeteaseLinkKind = Literal["song", "album", "playlist"]
BASE = "https://music.163.com/"
_KINDS = ("song", "album", "playlist")
_DIGITS = re.compile(r"^\d+$")
def _is_digits(value: str) -> bool:
return value != "" and _DIGITS.match(value) is not None
def _parse_candidate(candidate: str) -> tuple[str, str]:
"""解析单个候选串(JS 需给相对候选补 base;Go 的 url.Parse 直接接受相对路径)。"""
try:
parsed = urlparse(urljoin(BASE, candidate))
except ValueError:
return "", ""
kind = ""
segments = [seg for seg in parsed.path.lower().split("/") if seg]
# 取最后一个出现的类型段:/playlist/xxx 之类多余段不影响判断
for segment in segments:
if segment in _KINDS:
kind = segment
id_ = parse_qs(parsed.query).get("id", [""])[0]
if not _is_digits(id_):
id_ = ""
if not kind and len(segments) >= 2:
last, prev = segments[-1], segments[-2]
if _is_digits(last) and prev in _KINDS:
kind, id_ = prev, last
if not kind or not id_:
return "", ""
return kind, id_
def parse_netease_link(link: Any) -> tuple[NeteaseLinkKind, str]:
"""链接 → (类型, id)。无法识别时抛 ValueError(文案与原文一致)。"""
raw = str(link or "").strip()
candidates = [raw]
try:
parsed = urlparse(urljoin(BASE, raw))
except ValueError:
parsed = None
if parsed is not None:
if parsed.path and parsed.path != "/":
candidates.append(parsed.path + (f"?{parsed.query}" if parsed.query else ""))
fragment = re.sub(r"^#!?", "", parsed.fragment).strip()
if fragment:
candidates.append(fragment)
for candidate in candidates:
kind, id_ = _parse_candidate(candidate)
if kind and id_:
return kind, id_ # type: ignore[return-value]
raise ValueError("无效的网易云链接")
def bare_id(link: Any) -> str:
"""裸 id 兜底:非 URL 且全数字时视为对应类型的 id。"""
raw = str(link or "").strip()
return raw if _DIGITS.match(raw) else ""