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,158 @@
|
||||
"""通用工具函数(对应原 sdk/common/utils.ts 与 sdk/utils.ts)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Iterable, Mapping
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
def to_md5(text: str) -> str:
|
||||
return hashlib.md5(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def as_dict(value: Any) -> dict[str, Any]:
|
||||
"""上游 ``x?.field ?? {}`` 的安全取法:非 dict 一律当空 dict。"""
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def as_list(value: Any) -> list[Any]:
|
||||
"""上游 ``x ?? []`` 的安全取法:非 list 一律当空列表。"""
|
||||
return value if isinstance(value, list) else []
|
||||
|
||||
|
||||
def size_formate(size: float | int | None) -> str:
|
||||
"""将字节数格式化为可读字符串。"""
|
||||
if not size:
|
||||
return "0 B"
|
||||
units = ["B", "KB", "MB", "GB", "TB"]
|
||||
number = math.floor(math.log(size) / math.log(1024))
|
||||
number = min(number, len(units) - 1)
|
||||
return f"{size / math.pow(1024, number):.2f} {units[number]}"
|
||||
|
||||
|
||||
def format_play_time(seconds: float | int | str | None) -> str:
|
||||
"""秒数 → mm:ss;0 返回 --/--。"""
|
||||
try:
|
||||
value = float(seconds) # type: ignore[arg-type]
|
||||
except (TypeError, ValueError):
|
||||
return "--/--"
|
||||
if math.isnan(value) or value < 0:
|
||||
value = 0
|
||||
minutes = int(value // 60)
|
||||
secs = int(value % 60)
|
||||
if minutes == 0 and secs == 0:
|
||||
return "--/--"
|
||||
return f"{minutes:02d}:{secs:02d}"
|
||||
|
||||
|
||||
def date_format(value: Any, fmt: str = "Y-M-D h:m:s") -> str:
|
||||
"""时间对象/时间戳/字符串 → 日期字符串(Y/M/D/h/m/s 占位)。"""
|
||||
if not value:
|
||||
return ""
|
||||
if isinstance(value, datetime):
|
||||
date = value
|
||||
elif isinstance(value, (int, float)):
|
||||
date = datetime.fromtimestamp(value / 1000 if value > 1e11 else value)
|
||||
elif isinstance(value, str):
|
||||
raw = value
|
||||
if "T" not in raw:
|
||||
raw = raw.split(".")[0].replace("-", "/")
|
||||
parsed: datetime | None = None
|
||||
for pattern in ("%Y/%m/%d %H:%M:%S", "%Y/%m/%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"):
|
||||
try:
|
||||
parsed = datetime.strptime(raw[: len(pattern) + 10], pattern)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
if parsed is None:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return ""
|
||||
date = parsed
|
||||
else:
|
||||
return ""
|
||||
return (
|
||||
fmt.replace("Y", str(date.year))
|
||||
.replace("M", f"{date.month:02d}")
|
||||
.replace("D", f"{date.day:02d}")
|
||||
.replace("h", f"{date.hour:02d}")
|
||||
.replace("m", f"{date.minute:02d}")
|
||||
.replace("s", f"{date.second:02d}")
|
||||
)
|
||||
|
||||
|
||||
def date_format2(timestamp_ms: float | int) -> str:
|
||||
"""相对时间格式化(用于评论时间)。"""
|
||||
differ = int((time.time() * 1000 - float(timestamp_ms)) / 1000)
|
||||
if differ < 60:
|
||||
return f"{differ}秒前"
|
||||
if differ < 3600:
|
||||
return f"{differ // 60}分钟前"
|
||||
if differ < 86400:
|
||||
return f"{differ // 3600}小时前"
|
||||
if differ < 2592000:
|
||||
return f"{differ // 86400}天前"
|
||||
return date_format(timestamp_ms)
|
||||
|
||||
|
||||
def format_play_count(num: float | int | None) -> str:
|
||||
if num is None:
|
||||
return "0"
|
||||
if num > 100000000:
|
||||
return f"{int(num / 10000000) / 10}亿"
|
||||
if num > 10000:
|
||||
return f"{int(num / 1000) / 10}万"
|
||||
return str(num)
|
||||
|
||||
|
||||
_ENTITY_MAP = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
""": '"',
|
||||
"'": "'",
|
||||
"'": "'",
|
||||
" ": " ",
|
||||
}
|
||||
|
||||
|
||||
def decode_name(text: Any = "") -> str:
|
||||
"""解码 HTML 实体。"""
|
||||
if not text:
|
||||
return ""
|
||||
if not isinstance(text, str):
|
||||
text = str(text)
|
||||
for entity, char in _ENTITY_MAP.items():
|
||||
text = text.replace(entity, char)
|
||||
return re.sub(r"&#(\d+);", lambda m: chr(int(m.group(1))), text)
|
||||
|
||||
|
||||
def format_singer_name(
|
||||
singers: Iterable[Mapping[str, Any]] | str | None,
|
||||
name_key: str = "name",
|
||||
join: str = "、",
|
||||
) -> str:
|
||||
"""歌手数组或字符串 → 展示名(数组按 name_key 提取后 join)。"""
|
||||
if isinstance(singers, (list, tuple)):
|
||||
names = [str(item[name_key]) for item in singers if isinstance(item, Mapping) and item.get(name_key)]
|
||||
return decode_name(join.join(names))
|
||||
return decode_name(str(singers or ""))
|
||||
|
||||
|
||||
def pick(data: Any, *keys: str, default: Any = None) -> Any:
|
||||
"""沿键路径取值(dict 缺失返回 default),迁移时统一空值访问。"""
|
||||
cur = data
|
||||
for key in keys:
|
||||
if isinstance(cur, Mapping):
|
||||
cur = cur.get(key)
|
||||
else:
|
||||
return default
|
||||
if cur is None:
|
||||
return default
|
||||
return cur
|
||||
Reference in new issue
Block a user