Files
MusicServer/app/sdk/platforms/joox/__init__.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

86 lines
3.8 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.
"""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,
)