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,85 @@
|
||||
"""JOOX(joox)平台模块(对应 sdk/joox/index.ts)。
|
||||
|
||||
匿名可用面(2026-10 实测,www 站点需 X-Forwarded-For 东南亚 IP 绕过地区拦截):
|
||||
搜索 / 专辑(搜索+详情+链接解析)/ 歌单(搜索+详情+链接解析)/ 歌词 / 链接解析(歌曲)/
|
||||
取流(游客 128k MP3,VIP 曲目为 preview/refrain 片段)。
|
||||
|
||||
通道:
|
||||
* 搜索/专辑搜索/歌单搜索:cache.api.joox.com/openjoox/v3/search(**无签名**)
|
||||
* 取流/歌曲详情/专辑曲目/歌单曲目:cache.api.joox.com/openjoox2/v1(**需 md5 签名**,见 util.joox_sign)
|
||||
* 歌词:api.joox.com/web-fcgi-bin/web_lyric(旧接口仍可用;失败回退 openjoox2 lrc_content)
|
||||
* 分类/歌单/专辑 SSR 兜底:www.joox.com __NEXT_DATA__
|
||||
|
||||
userPlaylist:上游 joox/user_playlist.go 返回 ErrUserPlaylistsUnsupported,未实现(不造)。
|
||||
|
||||
⚠ 网络连通性实况(本机出口 IP 非东南亚):
|
||||
* cache.api.joox.com 与 api.joox.com 的 JSON 接口全部可直连;
|
||||
* www.joox.com 的 SSR 页面返回「Sorry, JOOX is not available in this location」,无 __NEXT_DATA__,
|
||||
因此 ``playlist.get_categories`` / ``playlist.get_category_playlists`` 与专辑·歌单的页面兜底
|
||||
在当前网络下会如实抛错(不返回假数据)。这与 TS 注释一致:TS 也依赖伪造 X-Forwarded-For,
|
||||
本移植带的是同一个头(``util.JOOX_XFF``),能否生效取决于上游是否采信,实测本机未被采信。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from app.sdk.apis.joox import get_music_url
|
||||
from app.sdk.auth_store import get_credentials
|
||||
from app.sdk.platforms._base import Platform
|
||||
from app.sdk.platforms.joox import album, lyric, music_search, parse, playlist
|
||||
from app.sdk.platforms.joox.music_info import get_track_detail
|
||||
from app.sdk.platforms.joox.util import joox_song_link, normalize_joox_id, pick_joox_image
|
||||
|
||||
|
||||
class JOOXSongList:
|
||||
"""契约别名 ``songList``(对应 TS index.ts 里的内联 shim)。
|
||||
|
||||
JOOX 没有独立的「排行榜/歌曲列表」接口:``getTags`` 即歌单分类,``getList`` 即某分类下的歌单。
|
||||
``sort_list`` 只有「全部」一项,因此路由透传的 ``sort_id`` 恒为空串,``tag_id`` 才是分类号。
|
||||
TS 的 ``getList(sortId, tagId, page, limit=30)`` 中 ``limit`` 固定在内部,路由不透传。
|
||||
"""
|
||||
|
||||
sort_list: ClassVar[list[dict[str, str]]] = [{"name": "全部", "id": ""}]
|
||||
|
||||
@staticmethod
|
||||
async def get_tags() -> dict[str, Any]:
|
||||
return await playlist.get_categories()
|
||||
|
||||
@staticmethod
|
||||
async def get_list(sort_id: str = "", tag_id: str = "", page: int = 1) -> dict[str, Any]:
|
||||
return await playlist.get_category_playlists(str(tag_id or sort_id or ""), page, 30)
|
||||
|
||||
|
||||
async def _get_pic(song_info: dict[str, Any]) -> str | None:
|
||||
"""封面(对应 TS index.ts 的 getPic):走 openjoox2 track 详情后挑图,取不到返回 None。"""
|
||||
song_id = normalize_joox_id(song_info.get("songmid") or song_info.get("id") or "")
|
||||
detail = await get_track_detail(song_id)
|
||||
return pick_joox_image(detail.get("images")) or None
|
||||
|
||||
|
||||
def get_music_detail_page_url(song_info: dict[str, Any]) -> str:
|
||||
return joox_song_link(song_info.get("songmid") or song_info.get("id") or "")
|
||||
|
||||
|
||||
def status() -> dict[str, Any]:
|
||||
"""登录状态(凭据存在性判断;无在线校验接口)。"""
|
||||
cred = get_credentials("joox")
|
||||
if cred and cred.get("cookie"):
|
||||
return {"logged": True, "updatedAt": cred.get("updatedAt")}
|
||||
return {"logged": False}
|
||||
|
||||
|
||||
MODULE = Platform(
|
||||
id="joox",
|
||||
name="JOOX",
|
||||
music_search=music_search,
|
||||
song_list=JOOXSongList,
|
||||
album=album,
|
||||
playlist=playlist,
|
||||
parse=parse,
|
||||
get_music_url=get_music_url,
|
||||
get_lyric=lyric.get_lyric,
|
||||
get_pic=_get_pic,
|
||||
get_music_detail_page_url=get_music_detail_page_url,
|
||||
)
|
||||
Reference in new issue
Block a user