211 行
6.8 KiB
Python
211 行
6.8 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
屏幕播放控制系统 - 后端服务
|
|
Screen Controller - Backend Server
|
|
|
|
一个用于展厅多屏幕定时播放控制的轻量级系统:
|
|
- 控制端(/)通过 Web 界面管理设备与播放列表
|
|
- 播放端(/player?device_id=xxx)按配置的时间定时播放图片/视频
|
|
- 支持局域网内多台设备同时访问
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
from datetime import datetime
|
|
|
|
from flask import (
|
|
Flask,
|
|
render_template,
|
|
request,
|
|
jsonify,
|
|
send_from_directory,
|
|
)
|
|
from werkzeug.utils import secure_filename
|
|
|
|
app = Flask(__name__)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 基础配置
|
|
# ---------------------------------------------------------------------------
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
|
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
|
UPLOAD_DIR = STATIC_DIR # 上传的媒体文件存放目录
|
|
|
|
# 允许上传的媒体类型(扩展名 -> 类型)
|
|
ALLOWED_EXTENSIONS = {
|
|
"png": "image",
|
|
"jpg": "image",
|
|
"jpeg": "image",
|
|
"gif": "image",
|
|
"webp": "image",
|
|
"bmp": "image",
|
|
"mp4": "video",
|
|
"webm": "video",
|
|
"ogg": "video",
|
|
"mov": "video",
|
|
"avi": "video",
|
|
"mkv": "video",
|
|
}
|
|
|
|
# 默认监听地址与端口(0.0.0.0 允许局域网访问)
|
|
HOST = os.environ.get("HOST", "0.0.0.0")
|
|
PORT = int(os.environ.get("PORT", "5000"))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 配置读写
|
|
# ---------------------------------------------------------------------------
|
|
def load_config():
|
|
"""加载配置文件;若不存在则返回默认结构。"""
|
|
if os.path.exists(CONFIG_FILE):
|
|
try:
|
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
if "devices" not in data or not isinstance(data["devices"], list):
|
|
data["devices"] = []
|
|
return data
|
|
except (json.JSONDecodeError, OSError):
|
|
return {"devices": []}
|
|
return {"devices": []}
|
|
|
|
|
|
def save_config(config):
|
|
"""保存配置到文件。"""
|
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
json.dump(config, f, ensure_ascii=False, indent=4)
|
|
|
|
|
|
def find_device(config, device_id):
|
|
"""在配置中查找指定 id 的设备。"""
|
|
return next((d for d in config.get("devices", []) if d.get("id") == device_id), None)
|
|
|
|
|
|
def allowed_file(filename):
|
|
"""判断文件扩展名是否在允许列表中。"""
|
|
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 页面路由
|
|
# ---------------------------------------------------------------------------
|
|
@app.route("/")
|
|
def index():
|
|
"""控制端:设备管理页面。"""
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route("/player")
|
|
@app.route("/player/<device_id>")
|
|
def player(device_id=None):
|
|
"""播放端页面。device_id 既可通过路径也可通过查询参数传入。"""
|
|
return render_template("player.html")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API:配置
|
|
# ---------------------------------------------------------------------------
|
|
@app.route("/get_config")
|
|
def get_config():
|
|
"""返回完整 JSON 配置。"""
|
|
return jsonify(load_config())
|
|
|
|
|
|
@app.route("/save_config", methods=["POST"])
|
|
def save_config_endpoint():
|
|
"""保存完整配置(来自控制端编辑)。"""
|
|
try:
|
|
data = request.get_json(force=True, silent=True)
|
|
if not isinstance(data, dict) or "devices" not in data:
|
|
return jsonify({"status": "error", "message": "配置格式不正确"}), 400
|
|
save_config(data)
|
|
return jsonify({"status": "success"})
|
|
except Exception as e: # noqa: BLE001
|
|
return jsonify({"status": "error", "message": str(e)}), 400
|
|
|
|
|
|
@app.route("/get_playlist/<device_id>")
|
|
def get_playlist(device_id):
|
|
"""播放端请求某个设备的播放列表。"""
|
|
config = load_config()
|
|
device = find_device(config, device_id)
|
|
if not device:
|
|
return jsonify({"error": "Device not found"}), 404
|
|
# 按 start_time 升序返回,便于前端调度
|
|
playlist = sorted(
|
|
device.get("playlist", []),
|
|
key=lambda x: x.get("start_time", ""),
|
|
)
|
|
return jsonify(
|
|
{
|
|
"device_id": device_id,
|
|
"name": device.get("name", device_id),
|
|
"playlist": playlist,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API:媒体文件
|
|
# ---------------------------------------------------------------------------
|
|
@app.route("/upload", methods=["POST"])
|
|
def upload_file():
|
|
"""上传媒体文件到 static 目录。"""
|
|
if "file" not in request.files:
|
|
return jsonify({"status": "error", "message": "未找到文件字段"}), 400
|
|
file = request.files["file"]
|
|
if file.filename == "":
|
|
return jsonify({"status": "error", "message": "未选择文件"}), 400
|
|
if not allowed_file(file.filename):
|
|
return jsonify({"status": "error", "message": "不支持的文件类型"}), 400
|
|
|
|
filename = secure_filename(file.filename)
|
|
# 避免重名覆盖
|
|
base, ext = os.path.splitext(filename)
|
|
counter = 1
|
|
target = os.path.join(UPLOAD_DIR, filename)
|
|
while os.path.exists(target):
|
|
filename = f"{base}_{counter}{ext}"
|
|
target = os.path.join(UPLOAD_DIR, filename)
|
|
counter += 1
|
|
|
|
file.save(target)
|
|
media_type = ALLOWED_EXTENSIONS[filename.rsplit(".", 1)[1].lower()]
|
|
return jsonify(
|
|
{
|
|
"status": "success",
|
|
"filename": filename,
|
|
"path": f"/static/{filename}",
|
|
"type": media_type,
|
|
}
|
|
)
|
|
|
|
|
|
@app.route("/files")
|
|
def list_files():
|
|
"""列出 static 目录下可用的媒体文件。"""
|
|
files = []
|
|
if os.path.isdir(STATIC_DIR):
|
|
for name in sorted(os.listdir(STATIC_DIR)):
|
|
ext = name.rsplit(".", 1)[-1].lower() if "." in name else ""
|
|
if ext in ALLOWED_EXTENSIONS:
|
|
files.append({"name": name, "path": f"/static/{name}", "type": ALLOWED_EXTENSIONS[ext]})
|
|
return jsonify({"files": files})
|
|
|
|
|
|
@app.route("/static/<path:filename>")
|
|
def serve_static(filename):
|
|
"""提供静态媒体文件访问。"""
|
|
return send_from_directory(STATIC_DIR, filename)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.makedirs(STATIC_DIR, exist_ok=True)
|
|
# 关闭 debug 以避免生产环境安全隐患;可通过环境变量 FLASK_DEBUG=1 开启
|
|
debug = os.environ.get("FLASK_DEBUG", "0") == "1"
|
|
print(f"屏幕控制系统已启动: http://{HOST}:{PORT}/ (局域网内其他设备可访问)")
|
|
app.run(host=HOST, port=PORT, debug=debug)
|