- server: FastAPI 后端(多屏管理、素材上传、节目编排、客户端注册/配置/心跳/崩溃上报、管理台托管) - android: Java 客户端(minSdk 23,全屏图片/视频轮播、远程配置、开机自启、崩溃上报) - web: React + Vite + antd 管理台(屏幕/素材/节目管理) - 屏幕设备 ID 关联机制、gunicorn 生产部署脚本
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
"""管理端:素材上传与管理。"""
|
|
import os
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..auth import get_current_user
|
|
from ..config import get_server_config
|
|
from ..database import get_db
|
|
from ..models import Asset, PlaylistItem
|
|
from ..schemas import AssetOut
|
|
|
|
router = APIRouter(prefix="/api/assets", tags=["assets"], dependencies=[Depends(get_current_user)])
|
|
|
|
_EXT_MAP = {
|
|
"image/jpeg": ".jpg",
|
|
"image/png": ".png",
|
|
"image/gif": ".gif",
|
|
"image/webp": ".webp",
|
|
"video/mp4": ".mp4",
|
|
"video/webm": ".webm",
|
|
"video/x-matroska": ".mkv",
|
|
}
|
|
|
|
|
|
@router.get("", response_model=list[AssetOut])
|
|
def list_assets(db: Session = Depends(get_db)):
|
|
return db.scalars(select(Asset).order_by(Asset.created_at.desc())).all()
|
|
|
|
|
|
@router.post("", response_model=AssetOut)
|
|
async def upload_asset(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
|
"""上传图片/视频素材。"""
|
|
cfg = get_server_config()
|
|
content_type = file.content_type or ""
|
|
ext = _EXT_MAP.get(content_type)
|
|
if not ext:
|
|
raise HTTPException(status_code=400, detail=f"不支持的素材类型: {content_type or '未知'}")
|
|
|
|
max_bytes = cfg.get("max_upload_mb", 500) * 1024 * 1024
|
|
# 流式读取并限制大小
|
|
data = b""
|
|
while chunk := await file.read(1024 * 1024):
|
|
data += chunk
|
|
if len(data) > max_bytes:
|
|
raise HTTPException(status_code=413, detail=f"文件超过大小限制 {cfg['max_upload_mb']}MB")
|
|
|
|
filename = f"{uuid.uuid4().hex}{ext}"
|
|
upload_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), cfg.get("upload_dir", "./uploads")))
|
|
os.makedirs(upload_dir, exist_ok=True)
|
|
dest = os.path.join(upload_dir, filename)
|
|
with open(dest, "wb") as f:
|
|
f.write(data)
|
|
|
|
is_video = content_type.startswith("video/")
|
|
asset = Asset(
|
|
name=file.filename or filename,
|
|
type="video" if is_video else "image",
|
|
url=f"/media/{filename}",
|
|
size=len(data),
|
|
duration=0 if is_video else 10, # 图片默认展示 10 秒
|
|
)
|
|
db.add(asset)
|
|
db.commit()
|
|
db.refresh(asset)
|
|
return asset
|
|
|
|
|
|
@router.delete("/{asset_id}")
|
|
def delete_asset(asset_id: int, db: Session = Depends(get_db)):
|
|
asset = db.get(Asset, asset_id)
|
|
if not asset:
|
|
raise HTTPException(status_code=404, detail="素材不存在")
|
|
|
|
used = db.scalar(select(func.count(PlaylistItem.id)).where(PlaylistItem.asset_id == asset_id))
|
|
if used:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"该素材正被 {used} 个节目引用,请先从节目中移除再删除",
|
|
)
|
|
|
|
cfg = get_server_config()
|
|
upload_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), cfg.get("upload_dir", "./uploads")))
|
|
file_path = os.path.join(upload_dir, os.path.basename(asset.url))
|
|
if os.path.exists(file_path):
|
|
os.remove(file_path)
|
|
|
|
db.delete(asset)
|
|
db.commit()
|
|
return {"ok": True}
|