Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""通过站内 OpenAPI 获取单个 Bilibili 视频,并以文件形式发送。"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
from urllib.parse import urlencode
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
CACHE_DIR = Path(BASE_DIR) / "bilibili" / "cache"
|
||||
OPENAPI_URL = "https://openapi.rrrosmontis.icu/bilibili/get_video"
|
||||
HTTP_TIMEOUT = (15, 600)
|
||||
MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024
|
||||
|
||||
HELP_TEXT = (
|
||||
"Bilibili 视频\n"
|
||||
"━━━━━━━━━━━━\n"
|
||||
"• .bilibili <BV号> 获取视频并以文件形式发送\n"
|
||||
"示例: .bilibili BV17XxP6CE7n\n"
|
||||
"━━━━━━━━━━━━\n"
|
||||
"默认请求最高画质;大会员或版权资源可能无法获取"
|
||||
)
|
||||
|
||||
|
||||
def sanitize_bv(text):
|
||||
"""只接受完整 BV 号,避免把任意文本或 URL 拼进 OpenAPI 请求。"""
|
||||
match = re.fullmatch(r"(?i:BV)[0-9A-Za-z]{10}", (text or "").strip())
|
||||
return "BV" + match.group(0)[2:] if match else None
|
||||
|
||||
|
||||
def openapi_url(bv):
|
||||
return OPENAPI_URL + "?" + urlencode({"bv": bv})
|
||||
|
||||
|
||||
def cleanup_file(local_path):
|
||||
try:
|
||||
if local_path:
|
||||
os.remove(local_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as exc:
|
||||
print(f"[bilibili] 临时文件清理失败: {exc}")
|
||||
|
||||
|
||||
def download_video(bv):
|
||||
"""流式下载到独立临时文件;任何失败都清理残缺文件。"""
|
||||
import requests
|
||||
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
local_path = None
|
||||
response = requests.get(
|
||||
OPENAPI_URL,
|
||||
params={"bv": bv},
|
||||
stream=True,
|
||||
timeout=HTTP_TIMEOUT,
|
||||
)
|
||||
try:
|
||||
if response.status_code != 200:
|
||||
detail = ""
|
||||
content_type = response.headers.get("Content-Type", "").lower()
|
||||
if "json" in content_type or "text" in content_type:
|
||||
detail = response.text[:240].strip()
|
||||
raise RuntimeError(
|
||||
f"OpenAPI 下载失败 HTTP {response.status_code}"
|
||||
+ (f": {detail}" if detail else "")
|
||||
)
|
||||
|
||||
content_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
|
||||
if "json" in content_type or content_type.startswith("text/"):
|
||||
raise RuntimeError(f"OpenAPI 未返回视频: {response.text[:240].strip()}")
|
||||
suffix = {
|
||||
"video/x-flv": ".flv",
|
||||
"video/webm": ".webm",
|
||||
"video/x-matroska": ".mkv",
|
||||
}.get(content_type, ".mp4")
|
||||
fd, local_path = tempfile.mkstemp(prefix=bv + "_", suffix=suffix, dir=CACHE_DIR)
|
||||
total = 0
|
||||
with os.fdopen(fd, "wb") as output:
|
||||
for chunk in response.iter_content(1024 * 1024):
|
||||
if not chunk:
|
||||
continue
|
||||
total += len(chunk)
|
||||
if total > MAX_DOWNLOAD_BYTES:
|
||||
raise RuntimeError("视频超过 512 MB,已停止下载")
|
||||
output.write(chunk)
|
||||
if total == 0:
|
||||
raise RuntimeError("OpenAPI 返回了空视频")
|
||||
return Path(local_path)
|
||||
except Exception:
|
||||
cleanup_file(local_path)
|
||||
raise
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
|
||||
async def handle_bilibili(channel, message_type, target_id, user_id, raw_text):
|
||||
from channel import send_message
|
||||
|
||||
body = re.sub(r"^\s*\.bilibili\b", "", raw_text, flags=re.IGNORECASE).strip()
|
||||
if not body or body.lower() in ("help", "-h", "帮助"):
|
||||
await send_message(channel, message_type, target_id, HELP_TEXT)
|
||||
return
|
||||
|
||||
bv = sanitize_bv(body)
|
||||
if not bv:
|
||||
await send_message(
|
||||
channel,
|
||||
message_type,
|
||||
target_id,
|
||||
"格式: .bilibili <BV号>\n示例: .bilibili BV17XxP6CE7n",
|
||||
)
|
||||
return
|
||||
|
||||
await send_message(channel, message_type, target_id, f"正在获取 Bilibili 视频 ({bv}),请稍候...")
|
||||
local_path = None
|
||||
try:
|
||||
local_path = await asyncio.to_thread(download_video, bv)
|
||||
ok = await send_message(
|
||||
channel,
|
||||
message_type,
|
||||
target_id,
|
||||
[{
|
||||
"type": "file",
|
||||
"data": {
|
||||
"file": local_path.resolve().as_uri(),
|
||||
"manual": openapi_url(bv),
|
||||
},
|
||||
}],
|
||||
)
|
||||
print(f"[bilibili] {bv} 文件投递{'成功' if ok else '失败'}")
|
||||
except Exception as exc:
|
||||
print(f"[bilibili] {bv} 获取失败: {type(exc).__name__}: {exc}")
|
||||
await send_message(
|
||||
channel,
|
||||
message_type,
|
||||
target_id,
|
||||
f"{bv} 视频获取失败:{str(exc)[:400]}",
|
||||
)
|
||||
finally:
|
||||
await asyncio.to_thread(cleanup_file, local_path)
|
||||
Reference in New Issue
Block a user