747 lines
25 KiB
Python
747 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, UploadFile, File
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from config.settings import get_config, update_config
|
|
from models.dashboard import dashboard_manager, DashboardTheme
|
|
from models.scene import scene_manager, Scene, SceneCanvas, DashboardPlacement
|
|
from server.telemetry.listener import telemetry_listener
|
|
from server.websocket import ws_manager
|
|
from utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/api")
|
|
|
|
|
|
# ---- Status ----
|
|
@router.get("/status")
|
|
async def get_status():
|
|
latest = telemetry_listener.latest_data
|
|
cfg = get_config()
|
|
return {
|
|
"server_running": True,
|
|
"telemetry_running": telemetry_listener.is_running,
|
|
"ws_clients": ws_manager.client_count,
|
|
"packet_count": telemetry_listener.packet_count,
|
|
"last_packet_time": telemetry_listener.last_packet_time,
|
|
"selected_game_id": cfg.get("selected_game_id"),
|
|
"forward_active": telemetry_listener.forwarder.active,
|
|
"latest_data": latest.to_dict() if latest else None,
|
|
}
|
|
|
|
|
|
# ---- Config ----
|
|
@router.get("/config")
|
|
async def api_get_config():
|
|
return get_config()
|
|
|
|
|
|
@router.put("/config")
|
|
async def api_update_config(data: dict[str, Any]):
|
|
cfg = get_config()
|
|
for k, v in data.items():
|
|
cfg[k] = v
|
|
from config.settings import save_config
|
|
save_config(cfg)
|
|
|
|
if "selected_game_id" in data:
|
|
telemetry_listener.set_parser_for_game(data["selected_game_id"])
|
|
|
|
if "theme" in data:
|
|
cfg["theme"] = data["theme"]
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
# ---- Telemetry ----
|
|
@router.post("/telemetry/start")
|
|
async def api_start_telemetry():
|
|
ok = await telemetry_listener.start()
|
|
return {"ok": ok, "running": telemetry_listener.is_running}
|
|
|
|
|
|
@router.post("/telemetry/stop")
|
|
async def api_stop_telemetry():
|
|
telemetry_listener.stop()
|
|
return {"ok": True, "running": telemetry_listener.is_running}
|
|
|
|
|
|
@router.post("/test-mode/start")
|
|
async def api_start_test_mode():
|
|
telemetry_listener.start_test_mode()
|
|
return {"ok": True, "testing": True}
|
|
|
|
|
|
@router.post("/test-mode/stop")
|
|
async def api_stop_test_mode():
|
|
telemetry_listener.stop_test_mode()
|
|
return {"ok": True, "testing": False}
|
|
|
|
|
|
@router.get("/telemetry/latest")
|
|
async def api_latest_telemetry():
|
|
latest = telemetry_listener.latest_data
|
|
if latest:
|
|
return {
|
|
"data": latest.to_dict(),
|
|
"raw": latest.raw,
|
|
}
|
|
return {"data": None, "raw": None}
|
|
|
|
|
|
# ---- Dashboards ----
|
|
@router.get("/dashboards")
|
|
async def api_list_dashboards(category: str = "all"):
|
|
return dashboard_manager.list_all(category)
|
|
|
|
|
|
@router.get("/dashboards/categories")
|
|
async def api_dashboard_categories():
|
|
return dashboard_manager.get_categories()
|
|
|
|
|
|
@router.get("/dashboards/{theme_id}")
|
|
async def api_get_dashboard(theme_id: str):
|
|
theme = dashboard_manager.get(theme_id)
|
|
if not theme:
|
|
raise HTTPException(404, "Dashboard not found")
|
|
return theme
|
|
|
|
|
|
@router.get("/dashboards/{theme_id}/template")
|
|
async def api_get_dashboard_template(theme_id: str):
|
|
tmpl = dashboard_manager.get_index_html(theme_id)
|
|
return {"html": tmpl or ""}
|
|
|
|
|
|
@router.post("/dashboards")
|
|
async def api_create_dashboard(data: dict[str, Any]):
|
|
cfg = data.get("config", {})
|
|
theme = DashboardTheme(
|
|
name=data.get("name", "Untitled"),
|
|
category=data.get("category", "basic"),
|
|
description=data.get("description", ""),
|
|
author=data.get("author", ""),
|
|
icon=cfg.get("icon", "📊"),
|
|
aspect_ratio=cfg.get("aspect_ratio", "auto"),
|
|
render_mode=cfg.get("render_mode", "contain"),
|
|
)
|
|
dashboard_manager.save(theme, data.get("index_html", ""))
|
|
return theme.to_dict()
|
|
|
|
|
|
@router.put("/dashboards/{theme_id}")
|
|
async def api_update_dashboard(theme_id: str, data: dict[str, Any]):
|
|
existing = dashboard_manager.get(theme_id)
|
|
if not existing:
|
|
raise HTTPException(404, "Dashboard not found")
|
|
theme = DashboardTheme(
|
|
id=theme_id,
|
|
name=data.get("name", existing.get("name", "")),
|
|
category=data.get("category", existing.get("category", "basic")),
|
|
description=data.get("description", existing.get("description", "")),
|
|
author=data.get("author", existing.get("author", "")),
|
|
icon=data.get("icon", existing.get("icon", "📊")),
|
|
aspect_ratio=data.get("aspect_ratio", existing.get("aspect_ratio", "auto")),
|
|
render_mode=data.get("render_mode", existing.get("render_mode", "contain")),
|
|
)
|
|
dashboard_manager.save(theme, data.get("index_html", ""))
|
|
return theme.to_dict()
|
|
|
|
|
|
@router.delete("/dashboards/{theme_id}")
|
|
async def api_delete_dashboard(theme_id: str):
|
|
ok = dashboard_manager.delete(theme_id)
|
|
return {"ok": ok}
|
|
|
|
|
|
@router.get("/dashboards/{theme_id}/export")
|
|
async def api_export_dashboard(theme_id: str):
|
|
from fastapi.responses import Response
|
|
zip_bytes = dashboard_manager.export_theme_zip(theme_id)
|
|
if not zip_bytes:
|
|
raise HTTPException(404, "Dashboard not found")
|
|
theme = dashboard_manager.get(theme_id)
|
|
filename = f"{theme['id']}.tsd" if theme else "dashboard.tsd"
|
|
return Response(
|
|
content=zip_bytes,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.post("/dashboards/import")
|
|
async def api_import_dashboard(file: UploadFile = File(...)):
|
|
if not file.filename or not file.filename.endswith('.tsd'):
|
|
raise HTTPException(400, "Only .tsd files are accepted")
|
|
zip_data = await file.read()
|
|
ok = dashboard_manager.import_theme_zip(zip_data)
|
|
return {"ok": ok}
|
|
|
|
|
|
# ---- Scenes ----
|
|
@router.get("/scenes")
|
|
async def api_list_scenes(game_id: str = ""):
|
|
return scene_manager.list_all(game_id or None)
|
|
|
|
|
|
@router.get("/scenes/{scene_id}")
|
|
async def api_get_scene(scene_id: str):
|
|
scene = scene_manager.get(scene_id)
|
|
if not scene:
|
|
raise HTTPException(404, "Scene not found")
|
|
return scene
|
|
|
|
|
|
@router.post("/scenes")
|
|
async def api_create_scene(data: dict[str, Any]):
|
|
scene = Scene(
|
|
name=data.get("name", "New Scene"),
|
|
game_id=data.get("game_id", ""),
|
|
description=data.get("description", ""),
|
|
canvases=[SceneCanvas(label="16:9", width=1920, height=1080)],
|
|
)
|
|
if data.get("canvases"):
|
|
scene.canvases = [
|
|
SceneCanvas(
|
|
width=c.get("width", 1920),
|
|
height=c.get("height", 1080),
|
|
label=c.get("label", ""),
|
|
placements=[DashboardPlacement(**p) for p in c.get("placements", [])],
|
|
)
|
|
for c in data["canvases"]
|
|
]
|
|
scene_manager.save(scene)
|
|
return scene.to_dict()
|
|
|
|
|
|
@router.put("/scenes/{scene_id}")
|
|
async def api_update_scene(scene_id: str, data: dict[str, Any]):
|
|
existing = scene_manager.get(scene_id)
|
|
if not existing:
|
|
raise HTTPException(404, "Scene not found")
|
|
scene = Scene.from_dict(existing)
|
|
for k in ["name", "game_id", "description"]:
|
|
if k in data:
|
|
setattr(scene, k, data[k])
|
|
if "canvases" in data:
|
|
scene.canvases = [
|
|
SceneCanvas(
|
|
width=c.get("width", 1920),
|
|
height=c.get("height", 1080),
|
|
label=c.get("label", "Custom"),
|
|
placements=[DashboardPlacement(**p) for p in c.get("placements", [])],
|
|
)
|
|
for c in data["canvases"]
|
|
]
|
|
scene_manager.save(scene)
|
|
return scene.to_dict()
|
|
|
|
|
|
@router.delete("/scenes/{scene_id}")
|
|
async def api_delete_scene(scene_id: str):
|
|
ok = scene_manager.delete(scene_id)
|
|
return {"ok": ok}
|
|
|
|
|
|
@router.get("/scenes/{scene_id}/export")
|
|
async def api_export_scene(scene_id: str):
|
|
from fastapi.responses import Response
|
|
zip_bytes = scene_manager.export_scene_zip(scene_id)
|
|
if not zip_bytes:
|
|
raise HTTPException(404, "Scene not found")
|
|
scene = scene_manager.get(scene_id)
|
|
filename = f"{scene['id']}.tss" if scene else "scene.tss"
|
|
return Response(
|
|
content=zip_bytes,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.post("/scenes/import")
|
|
async def api_import_scene(file: UploadFile = File(...)):
|
|
if not file.filename or not file.filename.endswith('.tss'):
|
|
raise HTTPException(400, "Only .tss files are accepted")
|
|
zip_data = await file.read()
|
|
ok = scene_manager.import_scene_zip(zip_data)
|
|
return {"ok": ok}
|
|
|
|
|
|
# ---- Game Plugins ----
|
|
from server.game_manager import game_plugin_manager
|
|
|
|
|
|
@router.get("/games")
|
|
async def api_list_games():
|
|
return game_plugin_manager.list_all()
|
|
|
|
|
|
@router.get("/games/{plugin_id}")
|
|
async def api_get_game(plugin_id: str):
|
|
gp = game_plugin_manager.get(plugin_id)
|
|
if not gp:
|
|
raise HTTPException(404, "Game plugin not found")
|
|
return gp.to_dict()
|
|
|
|
|
|
@router.post("/games/install")
|
|
async def api_install_game_plugin(file: UploadFile = File(...)):
|
|
if not file.filename or not file.filename.endswith('.tsp'):
|
|
raise HTTPException(400, "Only .tsp files are accepted")
|
|
zip_data = await file.read()
|
|
ok = game_plugin_manager.install_plugin_zip(zip_data)
|
|
return {"ok": ok}
|
|
|
|
|
|
@router.delete("/games/{plugin_id}")
|
|
async def api_remove_game_plugin(plugin_id: str):
|
|
ok = game_plugin_manager.remove_plugin(plugin_id)
|
|
return {"ok": ok}
|
|
|
|
|
|
@router.get("/games/{plugin_id}/export")
|
|
async def api_export_game_plugin(plugin_id: str):
|
|
from fastapi.responses import Response
|
|
zip_bytes = game_plugin_manager.export_plugin_zip(plugin_id)
|
|
if not zip_bytes:
|
|
raise HTTPException(404, "Game plugin not found")
|
|
return Response(
|
|
content=zip_bytes,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{plugin_id}.tsp"'},
|
|
)
|
|
|
|
|
|
@router.post("/games/reload")
|
|
async def api_reload_game_plugins():
|
|
game_plugin_manager.reload()
|
|
return {"ok": True, "count": len(game_plugin_manager.list_all())}
|
|
|
|
|
|
# ---- Logs ----
|
|
@router.get("/logs")
|
|
async def api_get_logs(lines: int = 50):
|
|
from pathlib import Path
|
|
log_file = Path(__file__).resolve().parent.parent / "logs" / "turbosu.log"
|
|
if not log_file.exists():
|
|
return {"lines": []}
|
|
try:
|
|
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
|
|
all_lines = f.readlines()
|
|
return {"lines": [l.rstrip() for l in all_lines[-lines:]]}
|
|
except Exception:
|
|
return {"lines": []}
|
|
|
|
|
|
# ---- Full Backup (.tsb) ----
|
|
@router.get("/backup/export")
|
|
async def api_export_backup():
|
|
import io, zipfile
|
|
from fastapi.responses import Response
|
|
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
base = Path(__file__).resolve().parent.parent
|
|
data_dir = base / "data"
|
|
if data_dir.exists():
|
|
for f in data_dir.rglob("*"):
|
|
if f.is_file() and "__pycache__" not in str(f):
|
|
zf.write(f, f.relative_to(base))
|
|
dash_dir = base / "dashboards"
|
|
if dash_dir.exists():
|
|
for f in dash_dir.rglob("*"):
|
|
if f.is_file():
|
|
zf.write(f, f.relative_to(base))
|
|
games_dir = base / "games" / "user"
|
|
if games_dir.exists():
|
|
for f in games_dir.rglob("*"):
|
|
if f.is_file():
|
|
zf.write(f, f.relative_to(base))
|
|
|
|
return Response(content=buf.getvalue(), media_type="application/zip",
|
|
headers={"Content-Disposition": "attachment; filename=turbosu_backup.tsb"})
|
|
|
|
|
|
@router.post("/backup/import")
|
|
async def api_import_backup(file: UploadFile = File(...)):
|
|
if not file.filename or not file.filename.endswith('.tsb'):
|
|
raise HTTPException(400, "Only .tsb files are accepted")
|
|
|
|
import io, zipfile, shutil
|
|
zip_data = await file.read()
|
|
base = Path(__file__).resolve().parent.parent
|
|
|
|
try:
|
|
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
|
for name in zf.namelist():
|
|
if name.endswith('/') or '__pycache__' in name:
|
|
continue
|
|
dest = base / name
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
dest.write_bytes(zf.read(name))
|
|
|
|
from config.settings import reload_config
|
|
reload_config()
|
|
from server.game_manager import game_plugin_manager
|
|
game_plugin_manager.reload()
|
|
|
|
return {"ok": True, "message": "备份已恢复,请重启服务"}
|
|
except Exception as e:
|
|
return {"ok": False, "message": str(e)}
|
|
|
|
|
|
# ---- Restart ----
|
|
@router.post("/restart")
|
|
async def api_restart():
|
|
import os, sys, atexit
|
|
logger.info("Restart requested via API")
|
|
atexit.register(lambda: os.execv(sys.executable, [sys.executable] + sys.argv))
|
|
sys.exit(0)
|
|
|
|
|
|
# ---- Data Forwarding ----
|
|
@router.get("/forward")
|
|
async def api_list_forward_targets():
|
|
cfg = get_config()
|
|
return cfg.get("forward_targets", [])
|
|
|
|
|
|
@router.put("/forward")
|
|
async def api_update_forward_targets(data: dict[str, Any]):
|
|
targets = data.get("targets", [])
|
|
cfg = get_config()
|
|
cfg["forward_targets"] = targets
|
|
from config.settings import save_config
|
|
save_config(cfg)
|
|
telemetry_listener.forwarder.reload_targets()
|
|
return {"ok": True, "active": telemetry_listener.forwarder.active}
|
|
|
|
|
|
# ---- Record & Playback ----
|
|
from server.recorder import TelemetryRecorder, TelemetryPlayer
|
|
|
|
_recorder = None
|
|
_player = None
|
|
|
|
def _get_recorder():
|
|
global _recorder
|
|
if _recorder is None: _recorder = TelemetryRecorder(telemetry_listener)
|
|
return _recorder
|
|
|
|
def _get_player():
|
|
global _player
|
|
if _player is None: _player = TelemetryPlayer(telemetry_listener)
|
|
return _player
|
|
|
|
@router.post("/record/start")
|
|
async def api_start_record():
|
|
_get_recorder().start()
|
|
return {"ok": True, "recording": True}
|
|
|
|
@router.post("/record/stop")
|
|
async def api_stop_record():
|
|
_get_recorder().stop()
|
|
return {"ok": True, "recording": False}
|
|
|
|
@router.get("/record/status")
|
|
async def api_record_status():
|
|
return {"recording": _get_recorder().is_recording, "playing": _get_player().is_playing}
|
|
|
|
@router.get("/recordings")
|
|
async def api_list_recordings():
|
|
return TelemetryPlayer.list_recordings()
|
|
|
|
@router.post("/playback/start")
|
|
async def api_start_playback(data: dict):
|
|
filename = data.get("filename", "")
|
|
if not filename: raise HTTPException(400, "filename required")
|
|
_get_player().start(filename)
|
|
return {"ok": True, "playing": True}
|
|
|
|
@router.post("/playback/stop")
|
|
async def api_stop_playback():
|
|
_get_player().stop()
|
|
return {"ok": True, "playing": False}
|
|
|
|
|
|
@router.get("/recording/{filename}/export")
|
|
async def api_export_recording(filename: str):
|
|
from fastapi.responses import FileResponse
|
|
from server.recorder import RECORDINGS_DIR
|
|
path = RECORDINGS_DIR / filename
|
|
if not path.exists():
|
|
raise HTTPException(404, "Recording not found")
|
|
return FileResponse(path, media_type="application/octet-stream", filename=filename)
|
|
|
|
|
|
@router.delete("/recording/{filename}/delete")
|
|
async def api_delete_recording(filename: str):
|
|
from server.recorder import RECORDINGS_DIR
|
|
path = RECORDINGS_DIR / filename
|
|
if path.exists(): path.unlink()
|
|
return {"ok": True}
|
|
|
|
|
|
# ---- Market Proxy ----
|
|
@router.get("/market/dashboards")
|
|
async def api_market_list():
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.get(f"{url}/api/collections/dashboards/records?sort=-name&perPage=50")
|
|
return resp.json()
|
|
except Exception:
|
|
return {"items": []}
|
|
|
|
|
|
@router.post("/market/auth")
|
|
async def api_market_auth(data: dict):
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.post(
|
|
f"{url}/api/collections/users/auth-with-password",
|
|
json={"identity": data.get("email", data.get("identity","")), "password": data.get("password")}
|
|
)
|
|
result = resp.json()
|
|
return result
|
|
except Exception as e:
|
|
return {"message": "服务器连接失败: " + str(e), "token": None}
|
|
|
|
|
|
@router.post("/market/register")
|
|
async def api_market_register(data: dict):
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.post(
|
|
f"{url}/api/collections/users/records",
|
|
json={
|
|
"email": data.get("email"),
|
|
"password": data.get("password"),
|
|
"passwordConfirm": data.get("passwordConfirm"),
|
|
"name": data.get("name", ""),
|
|
}
|
|
)
|
|
return resp.json()
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.post("/market/upload")
|
|
async def api_market_upload(file: UploadFile = File(...), name: str = "", category: str = "", author: str = "", token: str = ""):
|
|
import httpx, io, zipfile, json as _json
|
|
|
|
# Validate .tsd
|
|
if not file.filename or not file.filename.endswith('.tsd'):
|
|
return {"error": "仅支持 .tsd 文件"}
|
|
|
|
try:
|
|
zip_data = await file.read()
|
|
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
|
names = zf.namelist()
|
|
if 'manifest.json' not in names:
|
|
return {"error": "无效的仪表盘文件:缺少 manifest.json"}
|
|
if 'index.html' not in names:
|
|
return {"error": "无效的仪表盘文件:缺少 index.html"}
|
|
manifest = _json.loads(zf.read('manifest.json').decode('utf-8'))
|
|
|
|
if not manifest.get('id'):
|
|
return {"error": "manifest.json 缺少 id"}
|
|
if not manifest.get('name'):
|
|
return {"error": "manifest.json 缺少 name"}
|
|
|
|
actual_name = name or manifest.get('name', file.filename.replace('.tsd', ''))
|
|
actual_category = category or manifest.get('category', 'community')
|
|
actual_author = author or manifest.get('author', 'Unknown')
|
|
actual_version = manifest.get('version', '1.0.0')
|
|
supported = manifest.get('supported_games', 'all')
|
|
icon = manifest.get('config', {}).get('icon', '📦')
|
|
desc = manifest.get('description', '')
|
|
except zipfile.BadZipFile:
|
|
return {"error": "无效的 zip 文件"}
|
|
except Exception as e:
|
|
return {"error": f"文件解析失败: {str(e)}"}
|
|
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
resp = await client.post(
|
|
f"{url}/api/collections/dashboards/records",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
data={
|
|
"name": actual_name, "category": actual_category, "author": actual_author,
|
|
"version": actual_version, "supported_games": supported,
|
|
"icon": icon, "description": desc, "dashb_pending": "true",
|
|
"manifest_id": manifest.get("id", ""),
|
|
},
|
|
files={"file": (file.filename, zip_data, file.content_type)},
|
|
)
|
|
return resp.json()
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/market/install")
|
|
async def api_market_install(id: str = "", filename: str = ""):
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
|
|
resp = await client.get(f"{url}/api/files/dashboards/{id}/{filename}")
|
|
zip_data = resp.content
|
|
from models.dashboard import dashboard_manager
|
|
ok = dashboard_manager.import_theme_zip(zip_data)
|
|
|
|
if ok:
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as client2:
|
|
# Get current downloads
|
|
r = await client2.get(f"{url}/api/collections/dashboards/records/{id}")
|
|
current = r.json().get("downloads", 0) or 0
|
|
# Increment
|
|
await client2.patch(
|
|
f"{url}/api/collections/dashboards/records/{id}",
|
|
json={"downloads": current + 1}
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return {"ok": ok}
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
|
|
# ---- Market Scenes ----
|
|
@router.get("/market/scenes")
|
|
async def api_market_scenes():
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.get(f"{url}/api/collections/scenes/records?sort=-name&perPage=50")
|
|
return resp.json()
|
|
except Exception:
|
|
return {"items": []}
|
|
|
|
|
|
@router.post("/market/scenes/upload")
|
|
async def api_market_upload_scene(file: UploadFile = File(...), token: str = ""):
|
|
import io, zipfile, json as _json
|
|
if not file.filename or not file.filename.endswith('.tss'):
|
|
return {"error": "仅支持 .tss 文件"}
|
|
try:
|
|
zip_data = await file.read()
|
|
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
|
if 'scene.json' not in zf.namelist():
|
|
return {"error": "缺少 scene.json"}
|
|
scene = _json.loads(zf.read('scene.json').decode('utf-8'))
|
|
if not scene.get('name'):
|
|
return {"error": "scene.json 缺少 name"}
|
|
deps = set()
|
|
for canvas in scene.get('canvases', []):
|
|
for p in canvas.get('placements', []):
|
|
if p.get('dashboard_id'):
|
|
deps.add(p['dashboard_id'])
|
|
except zipfile.BadZipFile:
|
|
return {"error": "无效的 zip 文件"}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
import httpx as _httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with _httpx.AsyncClient(timeout=30) as client:
|
|
resp = await client.post(
|
|
f"{url}/api/collections/scenes/records",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
data={"name": scene.get('name'), "description": scene.get('description',''), "game_id": scene.get('game_id',''), "author": scene.get('author','') or "Unknown", "dashb_pending": True},
|
|
files={"file": (file.filename, zip_data, file.content_type)},
|
|
)
|
|
result = resp.json()
|
|
result["dependencies"] = list(deps)
|
|
return result
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
@router.get("/market/scenes/install")
|
|
async def api_market_install_scene(id: str = "", filename: str = ""):
|
|
import httpx, io, zipfile, json as _json
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
|
|
resp = await client.get(f"{url}/api/files/scenes/{id}/{filename}")
|
|
zip_data = resp.content
|
|
missing = []
|
|
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
|
if 'scene.json' in zf.namelist():
|
|
scene = _json.loads(zf.read('scene.json').decode('utf-8'))
|
|
from models.dashboard import dashboard_manager
|
|
for canvas in scene.get('canvases', []):
|
|
for p in canvas.get('placements', []):
|
|
did = p.get('dashboard_id', '')
|
|
if did and not dashboard_manager.get(did):
|
|
missing.append(did)
|
|
from models.scene import scene_manager
|
|
ok = scene_manager.import_scene_zip(zip_data)
|
|
return {"ok": ok, "missing_dashboards": missing}
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
|
|
@router.get("/market/mine")
|
|
async def api_market_mine(token: str = ""):
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
result = {"dashboards": [], "scenes": []}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
r = await client.get(f"{url}/api/collections/dashboards/records?sort=-name&perPage=100",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
result["dashboards"] = r.json().get("items", [])
|
|
r = await client.get(f"{url}/api/collections/scenes/records?sort=-name&perPage=100",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
result["scenes"] = r.json().get("items", [])
|
|
except Exception:
|
|
pass
|
|
return result
|
|
|
|
|
|
@router.post("/market/delete")
|
|
async def api_market_delete(data: dict):
|
|
import httpx
|
|
cfg = get_config()
|
|
url = cfg.get("market_url", "http://127.0.0.1:5301")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
collection = data.get("collection", "dashboards")
|
|
record_id = data.get("id", "")
|
|
token = data.get("token", "")
|
|
resp = await client.delete(
|
|
f"{url}/api/collections/{collection}/records/{record_id}",
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
)
|
|
return {"ok": resp.status_code < 400}
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|