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

+134
View File
@@ -0,0 +1,134 @@
"""Apple Music 平台模块(对应 sdk/apple/index.ts,上游 music-lib/apple/apple.go 的 Node/TS 移植)。
能力:搜索 / 歌词(TTML,匿名 developer token 通常拿不到)/ 封面 /
专辑(搜索+详情+链接解析)/ 歌单(搜索+详情+分类+分类歌单+链接解析)/
链接解析(song/album/playlist)/ 取流(**仅 preview 试听片段**,见 apis/apple.py)。
凭据:auth_store 的 apple 记录,cookie 形如
``"media-user-token=xxx;storefront=cn;token=xxx"``,也支持 storefront/mediaUserToken/token 直填字段。
未实现(上游也没有):userPlaylist、榜单、热搜、歌手、评论、扫码登录。
"""
from __future__ import annotations
from typing import Any, ClassVar
from app.sdk.auth_store import get_credentials
from app.sdk.platforms._base import Platform
from app.sdk.platforms.apple import album, lyric, music_search, parse, playlist
from app.sdk.platforms.apple.util import (
apple_artwork_url,
apple_song_from_catalog,
apple_web_url,
extract_apple_id,
fetch_apple_song_resource,
get_apple_config,
get_apple_token,
)
def _song_id_of(song_info: dict[str, Any]) -> str:
return extract_apple_id(str(song_info.get("songmid") or song_info.get("id") or ""), "song")
async def _init() -> bool:
"""预热 developer token(失败不阻塞启动,路由调用时会重试)。"""
try:
await get_apple_token()
return True
except Exception: # noqa: BLE001 - token 预热失败不影响后续请求的重试
return False
async def _get_music_url(song_info: dict[str, Any], quality: str, retry_num: int = 0) -> dict[str, Any]:
from app.sdk.apis.apple import get_music_url
return await get_music_url(song_info, quality, retry_num)
async def get_lyric(song_info: dict[str, Any]) -> dict[str, str]:
return await lyric.get_lyric(song_info)
async def get_pic(song_info: dict[str, Any]) -> str | None:
"""封面:搜索结果自带 img;缺失时回源歌曲详情(best-effort)。"""
if song_info.get("img"):
return str(song_info["img"])
song_id = _song_id_of(song_info)
if not song_id:
return None
try:
res = await fetch_apple_song_resource(song_id)
if not res:
return None
return apple_artwork_url(res.get("attributes", {}).get("artwork"), 600) or None
except Exception: # noqa: BLE001 - 封面取不到返回 None
return None
async def get_music_info(song_info: dict[str, Any]) -> dict[str, Any] | None:
"""歌曲详情(best-effort;等价上游 Parse(id))。"""
song_id = _song_id_of(song_info)
if not song_id:
return None
res = await fetch_apple_song_resource(song_id, "lyrics,albums")
return apple_song_from_catalog(res) if res else None
class _AppleAlbum:
"""契约 album 能力位(含 TS index.ts 里挂的 parseAlbumLink)。"""
get_album_detail = staticmethod(album.get_album_detail)
get_album_songs = staticmethod(album.get_album_songs)
search_album = staticmethod(album.search_album)
parse_album_link = staticmethod(album.parse_album_link)
class _AppleSongList:
"""契约别名 songList:Apple 的歌单分类即 genre curators,供 /song-list/apple 复用。"""
sort_list: ClassVar[list[dict[str, str]]] = [{"name": "热门", "id": "1526756058"}]
@staticmethod
async def get_tags() -> dict[str, Any]:
return await playlist.get_categories()
@staticmethod
async def get_list(sort_id: Any, tag_id: Any, page: int = 1, limit: int = 20) -> dict[str, Any]:
cate = str(tag_id or sort_id or "").strip()
return await playlist.get_category_playlists(cate, page, limit)
def get_music_detail_page_url(song_info: dict[str, Any]) -> str:
if song_info.get("link"):
return str(song_info["link"])
song_id = _song_id_of(song_info)
if not song_id:
return "https://music.apple.com/"
return apple_web_url("song", song_id, get_apple_config()["storefront"])
def status() -> dict[str, Any]:
"""登录状态(media-user-token 存在性判断;无在线校验接口)。"""
cred = get_credentials("apple") or {}
cfg = get_apple_config()
if cfg["mediaUserToken"] or cred.get("cookie"):
return {"logged": True, "updatedAt": cred.get("updatedAt")}
return {"logged": False}
MODULE = Platform(
id="apple",
name="Apple Music",
init=_init,
music_search=music_search,
album=_AppleAlbum,
playlist=playlist,
song_list=_AppleSongList,
parse=parse,
get_music_url=_get_music_url,
get_lyric=get_lyric,
get_pic=get_pic,
get_music_info=get_music_info,
get_music_detail_page_url=get_music_detail_page_url,
)