Files
MusicServer/app/sdk/apis/apple.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

79 lines
3.6 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.
"""Apple Music 取流(对应 sdk/apis/apple.ts,上游 apple.go GetDownloadURL)。
⚠️ Apple 只提供 **preview 试听片段**(audio-ssl.itunes.apple.com,AAC/m4a,约 30-90 秒),
完整曲目是 FairPlay DRM 加密流(extendedAssetUrls.enhancedHls),需要 gamdl 等外部工具解密,
本 SDK 不提供。因此这里**任何请求档位都返回 type='preview'**,不假装是无损完整曲。
歌曲的 previews[0].url 来自 catalog 歌曲详情(搜索结果缓存里的 previewUrl 也会直接复用)。
统一返回:``{type:'preview', url, ext:'m4a', preview:true, songDuration, isrc}``
(songDuration 是歌曲完整时长,试听片段本身约 30-90 秒);
type 请求参数不影响结果(只有 preview 一档);supportQualitys 声明为 ['128k'] 以对齐
/sources 与 /url?quality 枚举,返回的实际档位以 type='preview' 为准。
"""
from __future__ import annotations
import math
from typing import Any
from app.sdk.common.utils import as_dict, as_list
from app.sdk.platforms.apple.util import apple_amp_get_tracked, extract_apple_id, get_apple_config
SUPPORT_QUALITYS: list[str] = ["128k"]
def _is_audio_url(value: Any) -> bool:
text = str(value if value is not None else "")
return text.startswith(("http://", "https://"))
def preview_from_attributes(attr: dict[str, Any]) -> dict[str, Any]:
"""详情 → preview 结果(内部/测试用)。"""
previews = as_list(attr.get("previews"))
url = str(as_dict(previews[0]).get("url") or "") if previews else ""
if not url:
raise RuntimeError("apple music: no preview URL available (完整曲目受 DRM 保护,SDK 仅提供 30-90 秒试听片段)")
try:
duration_ms = float(attr.get("durationInMillis") or 0)
except (TypeError, ValueError):
duration_ms = 0.0
return {
"type": "preview",
"url": url,
"ext": "m4a",
"preview": True,
# 注意:这是歌曲完整时长(catalog durationInMillis),不是试听片段长度
"songDuration": math.floor(duration_ms / 1000 + 0.5),
"isrc": attr.get("isrc") or "",
}
async def get_preview_url(song_id: str) -> dict[str, Any]:
"""按 song id 拉取 preview(上游 GetDownloadURL → Parse)。"""
cfg = get_apple_config()
body = as_dict(
await apple_amp_get_tracked(
f"/v1/catalog/{cfg['storefront']}/songs/{song_id}",
{"extend": "extendedAssetUrls"},
)
)
data = as_list(body.get("data"))
if not data:
raise RuntimeError(f"apple song not found: {song_id}")
return preview_from_attributes(as_dict(as_dict(data[0]).get("attributes")))
async def get_music_url(song_info: dict[str, Any], quality: str, retry_num: int = 0) -> dict[str, Any]:
"""统一取流:preview 直链优先复用,否则按 song id 拉取详情。"""
del quality, retry_num # 只有 preview 一档,请求档位不影响结果
# 上游 GetDownloadURL:Song.URL 已有直链时直接复用(搜索结果里的 previewUrl / 链接解析结果)
if _is_audio_url(song_info.get("previewUrl")):
return {"type": "preview", "url": str(song_info["previewUrl"]), "ext": "m4a", "preview": True}
if _is_audio_url(song_info.get("url")):
return {"type": "preview", "url": str(song_info["url"]), "ext": "m4a", "preview": True}
song_id = extract_apple_id(str(song_info.get("songmid") or song_info.get("id") or ""), "song")
if not song_id:
raise RuntimeError("apple 歌曲 id 无效(需要数字 song id 或 Apple Music 歌曲链接)")
return await get_preview_url(song_id)