- 新增 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 路由验证通过
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""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,
|
||
)
|