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

+110
View File
@@ -0,0 +1,110 @@
"""平台模块契约 —— 各平台包导出 `MODULE = Platform(...)`。
对应原 TS 的 `sdk/<platform>/index.ts` default export 对象:
const kw = { musicSearch, leaderboard, songList, hotSearch, comment, album, playlist, parse,
getMusicUrl, getLyric, getPic, handleMusicInfo, getMusicInfo, ... }
Python 侧用 `Platform` 容器承载子模块(属性名 snake_case),能力位缺省为 None 表示不支持,
路由层据此返回 404(与 /sources 的能力位同源)。
子模块协议:
music_search.search(keyword, page=1, limit=20, retry_num=0) -> {"list": [...], ...}
leaderboard.get_boards() -> {"list": [{"id", "name", "bangid"}]}
leaderboard.get_list(bangid, page=1) -> {"list", "total", "page", "limit", "source"}
song_list.get_list(sort_id, tag_id, page=1) -> {"list", "total", "page", "limit", "source"}
song_list.sort_list -> [{"id", "name"}](可选,路由取首项作默认排序)
hot_search.get_list() -> {"list": [str], "source"}
singer.get_info(id) / get_song_list(id, page, limit) / get_album_list(id, page, limit)
album.get_album_detail(id, page, limit) | get_album_list_detail(id, page)
album.search_album(keyword, page, limit)
playlist.get_playlist_detail(id, page, limit) | get_playlist_songs(id)
playlist.search_playlist(keyword, page, limit) | get_categories() | get_category_playlists(id, page, limit)
parse.parse_song(link) / parse_album(link) / parse_playlist(link)
user_playlist.get_list(page, limit)
顶层方法(由 Platform 直接持有):
get_music_url(song_info, quality, retry_num=0) -> {"type", "url"}
get_lyric(song_info) -> {"lyric", "tlyric", "rlyric"?, "yrc"?}
get_pic(song_info) -> str
get_music_info(song_info) -> dict(可选,酷我兜底等场景需要)
handle_music_info(song_info) -> dict(可选,补全歌名/歌手/封面)
get_music_detail_page_url(song_info) -> str(可选)
"""
from __future__ import annotations
from typing import Any
# 能力位名称(/sources 的 capabilities 键 → Platform 属性)
CAPABILITY_ATTRS = {
"search": "music_search",
"url": "get_music_url",
"lyric": "get_lyric",
"pic": "get_pic",
"boards": "leaderboard",
"hotSearch": "hot_search",
"songList": "song_list",
"singer": "singer",
"playlist": "playlist",
"parse": "parse",
"userPlaylist": "user_playlist",
"comment": "comment",
}
class Platform:
"""平台模块容器。只声明属性,不做校验 —— 缺省即不支持。"""
# 平台短码与中文名(/sources 用)
id: str = ""
name: str = ""
music_search: Any = None
leaderboard: Any = None
song_list: Any = None
hot_search: Any = None
comment: Any = None
singer: Any = None
album: Any = None
playlist: Any = None
parse: Any = None
user_playlist: Any = None
get_music_url: Any = None
get_lyric: Any = None
get_pic: Any = None
get_music_info: Any = None
handle_music_info: Any = None
get_music_urls: Any = None
get_music_detail_page_url: Any = None
init: Any = None
def __init__(self, **kwargs: Any) -> None:
for key, value in kwargs.items():
setattr(self, key, value)
def __repr__(self) -> str:
caps = [key for key in CAPABILITY_ATTRS.values() if getattr(self, key, None) is not None]
return f"<Platform {self.id} caps={','.join(caps)}>"
def capability_map(platform: Platform) -> dict[str, bool]:
"""生成 /sources 的 capabilities 字段(键名沿用原 API 的驼峰写法)。"""
caps: dict[str, bool] = {name: getattr(platform, attr, None) is not None for name, attr in CAPABILITY_ATTRS.items()}
album = platform.album
caps["album"] = album is not None and (
getattr(album, "get_album_detail", None) is not None
or getattr(album, "get_album_list_detail", None) is not None
)
caps["albumSearch"] = album is not None and getattr(album, "search_album", None) is not None
playlist = platform.playlist
caps["playlist"] = playlist is not None and getattr(playlist, "get_playlist_detail", None) is not None
caps["playlistSearch"] = playlist is not None and getattr(playlist, "search_playlist", None) is not None
caps["playlistCategories"] = playlist is not None and getattr(playlist, "get_categories", None) is not None
parse = platform.parse
caps["parse"] = parse is not None and any(
getattr(parse, f"parse_{kind}", None) is not None for kind in ("song", "album", "playlist")
)
caps.pop("comment", None)
return caps