| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- from flask import Flask, jsonify, request, render_template, send_from_directory
- from flask_cors import CORS
- import json
- import os
- import time
- import uuid
- import socket
- from werkzeug.utils import secure_filename
- from application.kodi_alive_thread import start_kodi_alive_check
- from application.kodi_free_time_thread import start_kodi_free_time_play
- from application.wled_thread import start_exhibit_led_effect, stop_led_effect, is_effect_running
- from application.kodi_thread import start_kodi_play, stop_kodi_play, is_kodi_thread_running, play_image, play_rtsp, revoke_individual_state, start_all_kodi_apps
- from utils.logger_config import logger
- app = Flask(__name__)
- CORS(app) # 允许跨域请求
- # 配置上传文件夹
- UPLOAD_FOLDER = 'uploads'
- ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
- app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上传文件大小为16MB
- # 确保上传文件夹存在
- if not os.path.exists(UPLOAD_FOLDER):
- os.makedirs(UPLOAD_FOLDER)
- def allowed_file(filename):
- """检查文件扩展名是否允许"""
- return '.' in filename and \
- filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
- def get_server_ip():
- """获取服务器的本地IP地址"""
- try:
- # 连接到一个远程地址(不会实际发送数据)
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.connect(("8.8.8.8", 80))
- ip = s.getsockname()[0]
- s.close()
- return ip
- except Exception:
- # 如果获取失败,尝试其他方法
- try:
- hostname = socket.gethostname()
- ip = socket.gethostbyname(hostname)
- return ip
- except Exception:
- return "127.0.0.1" # 回退到localhost
- @app.route('/')
- def index():
- """返回HTML页面"""
- return render_template('index.html')
- @app.route('/api/led/status', methods=['GET'])
- def get_led_status():
- """获取LED状态"""
- try:
- is_running = is_effect_running()
- return jsonify({
- "success": True,
- "data": {
- "is_running": is_running,
- "message": "灯效正在运行" if is_running else "灯效已停止"
- }
- })
- except Exception as e:
- return jsonify({
- "success": False,
- "message": f"获取状态失败: {str(e)}"
- }), 500
- @app.route('/api/led/start', methods=['POST'])
- def start_led_effect():
- """启动展品LED灯效控制"""
- try:
- data = request.get_json()
- if not data or 'exhibit_id' not in data:
- return jsonify({
- "success": False,
- "message": "缺少展品ID参数"
- }), 400
-
- exhibit_id = data['exhibit_id']
- if not isinstance(exhibit_id, int) or exhibit_id < 0:
- return jsonify({
- "success": False,
- "message": "展品ID必须是大于等于0的整数"
- }), 400
-
- success = start_exhibit_led_effect(exhibit_id)
- if success:
- return jsonify({
- "success": True,
- "message": f"展品 {exhibit_id} 的灯效已启动",
- "data": {
- "exhibit_id": exhibit_id,
- "is_running": True
- }
- })
- else:
- return jsonify({
- "success": False,
- "message": f"启动展品 {exhibit_id} 的灯效失败"
- }), 500
-
- except Exception as e:
- return jsonify({
- "success": False,
- "message": f"启动灯效失败: {str(e)}"
- }), 500
- @app.route('/api/led/stop', methods=['POST'])
- def stop_led_effect_api():
- """停止当前LED灯效"""
- try:
- success = stop_led_effect()
- if success:
- return jsonify({
- "success": True,
- "message": "灯效已停止",
- "data": {
- "is_running": False
- }
- })
- else:
- return jsonify({
- "success": False,
- "message": "停止灯效失败"
- }), 500
-
- except Exception as e:
- return jsonify({
- "success": False,
- "message": f"停止灯效失败: {str(e)}"
- }), 500
- # ===== Kodi 播放控制接口 =====
- @app.route('/api/kodi/status', methods=['GET'])
- def get_kodi_status():
- try:
- running = is_kodi_thread_running()
- return jsonify({
- "success": True,
- "data": {
- "is_running": running,
- "message": "Kodi播放线程运行中" if running else "Kodi播放线程已停止"
- }
- })
- except Exception as e:
- return jsonify({
- "success": False,
- "message": f"获取Kodi状态失败: {str(e)}"
- }), 500
- @app.route('/api/kodi/start', methods=['POST'])
- def start_kodi_play_api():
- try:
- data = request.get_json()
- if not data or 'video_id' not in data:
- return jsonify({
- "success": False,
- "message": "缺少视频ID参数"
- }), 400
- video_id = data['video_id']
- if not isinstance(video_id, int) or video_id < 0:
- return jsonify({
- "success": False,
- "message": "视频ID必须是大于等于0的整数"
- }), 400
- ok = start_kodi_play(video_id)
- if ok:
- return jsonify({
- "success": True,
- "message": f"Kodi开始/切换播放 视频ID={video_id}",
- "data": {"video_id": video_id}
- })
- return jsonify({
- "success": False,
- "message": f"Kodi播放启动失败(视频ID={video_id})"
- }), 500
- except Exception as e:
- return jsonify({
- "success": False,
- "message": f"Kodi播放启动异常: {str(e)}"
- }), 500
- # 指定某台kodi_client_index播放图片,这边要上传图片并且传递完整url给kodi播放
- @app.route('/api/kodi/play_image', methods=['POST'])
- def play_image_api():
- """播放图片接口,支持文件上传或直接传递图片URL"""
- try:
- # 检查是否有文件上传
- if 'file' in request.files:
- file = request.files['file']
- if file.filename == '':
- return jsonify({
- "success": False,
- "message": "未选择文件"
- }), 400
-
- if file and allowed_file(file.filename):
- # 获取原始文件扩展名
- original_filename = file.filename
- # 提取文件扩展名(不包含点号)
- if '.' in original_filename:
- file_ext = original_filename.rsplit('.', 1)[1].lower()
- else:
- # 如果没有扩展名,根据Content-Type推断或默认为jpg
- content_type = file.content_type or ''
- if 'png' in content_type:
- file_ext = 'png'
- elif 'jpeg' in content_type or 'jpg' in content_type:
- file_ext = 'jpg'
- elif 'gif' in content_type:
- file_ext = 'gif'
- else:
- file_ext = 'jpg' # 默认扩展名
-
- # 确保扩展名在允许列表中
- if file_ext not in ALLOWED_EXTENSIONS:
- file_ext = 'jpg'
-
- # 生成唯一文件名:使用时间戳和UUID,确保文件名唯一且安全
- timestamp = int(time.time() * 1000)
- unique_id = str(uuid.uuid4())[:8] # 使用UUID的前8位作为唯一标识
- filename = f"{timestamp}_{unique_id}.{file_ext}"
-
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
- file.save(filepath)
-
- # 生成可访问的URL(使用服务器的实际IP地址,而不是localhost)
- # 如果request.host包含localhost或127.0.0.1,使用实际IP
- host = request.host
- if 'localhost' in host or '127.0.0.1' in host:
- server_ip = get_server_ip()
- port = request.environ.get('SERVER_PORT', '5000')
- host = f"{server_ip}:{port}"
-
- image_url = f"http://{host}/uploads/{filename}"
- logger.info(f"图片已上传: {filepath}, URL: {image_url}, 原始文件名: {original_filename}")
- else:
- return jsonify({
- "success": False,
- "message": f"不支持的文件类型,允许的类型: {', '.join(ALLOWED_EXTENSIONS)}"
- }), 400
- elif request.is_json:
- # 检查是否有直接的图片URL
- data = request.get_json()
- if 'image_url' not in data:
- return jsonify({
- "success": False,
- "message": "缺少参数:需要 'file'(文件上传)或 'image_url'(图片URL)"
- }), 400
- image_url = data['image_url']
- if not image_url or not isinstance(image_url, str):
- return jsonify({
- "success": False,
- "message": "无效的图片URL"
- }), 400
- else:
- return jsonify({
- "success": False,
- "message": "缺少参数:需要 'file'(文件上传)或 'image_url'(图片URL)"
- }), 400
-
- # 获取客户端索引
- if 'kodi_client_index' in request.form:
- try:
- client_index = int(request.form['kodi_client_index'])
- except (ValueError, TypeError):
- return jsonify({
- "success": False,
- "message": "kodi_client_index 必须是整数"
- }), 400
- elif request.is_json and 'kodi_client_index' in request.get_json():
- client_index = request.get_json()['kodi_client_index']
- else:
- return jsonify({
- "success": False,
- "message": "缺少参数: kodi_client_index"
- }), 400
-
- if not isinstance(client_index, int) or client_index < 0:
- return jsonify({
- "success": False,
- "message": "kodi_client_index 必须是大于等于0的整数"
- }), 400
-
- # 调用播放函数
- success = play_image(image_url, client_index)
- if success:
- return jsonify({
- "success": True,
- "message": f"已在客户端 {client_index} 上启动图片播放",
- "data": {
- "image_url": image_url,
- "client_index": client_index
- }
- })
- else:
- return jsonify({
- "success": False,
- "message": f"在客户端 {client_index} 上启动图片播放失败"
- }), 500
-
- except Exception as e:
- logger.error(f"播放图片异常: {str(e)}")
- return jsonify({
- "success": False,
- "message": f"播放图片失败: {str(e)}"
- }), 500
- # 指定某台kodi_client_index播放rtsp视频
- @app.route('/api/kodi/play_rtsp', methods=['POST'])
- def play_rtsp_api():
- """播放RTSP视频流接口"""
- try:
- data = request.get_json()
- if not data:
- return jsonify({
- "success": False,
- "message": "请求体不能为空"
- }), 400
-
- # 检查必需的参数
- if 'rtsp_url' not in data:
- return jsonify({
- "success": False,
- "message": "缺少参数: rtsp_url"
- }), 400
-
- if 'kodi_client_index' not in data:
- return jsonify({
- "success": False,
- "message": "缺少参数: kodi_client_index"
- }), 400
-
- rtsp_url = data['rtsp_url']
- client_index = data['kodi_client_index']
- volume = data.get('volume', 0) # 可选参数,默认为0
-
- # 参数验证
- if not isinstance(rtsp_url, str) or not rtsp_url.strip():
- return jsonify({
- "success": False,
- "message": "rtsp_url 必须是有效的字符串"
- }), 400
-
- if not isinstance(client_index, int) or client_index < 0:
- return jsonify({
- "success": False,
- "message": "kodi_client_index 必须是大于等于0的整数"
- }), 400
-
- if not isinstance(volume, int) or volume < 0 or volume > 100:
- return jsonify({
- "success": False,
- "message": "volume 必须是 0-100 之间的整数"
- }), 400
-
- # 调用播放函数
- success = play_rtsp(rtsp_url, client_index, volume)
- if success:
- return jsonify({
- "success": True,
- "message": f"已在客户端 {client_index} 上启动RTSP播放",
- "data": {
- "rtsp_url": rtsp_url,
- "client_index": client_index,
- "volume": volume
- }
- })
- else:
- return jsonify({
- "success": False,
- "message": f"在客户端 {client_index} 上启动RTSP播放失败"
- }), 500
-
- except Exception as e:
- logger.error(f"播放RTSP异常: {str(e)}")
- return jsonify({
- "success": False,
- "message": f"播放RTSP失败: {str(e)}"
- }), 500
- @app.route('/api/kodi/revoke_individual_state', methods=['POST'])
- def revoke_individual_state_api():
- """撤销所有客户端的独立状态接口"""
- try:
- success = revoke_individual_state()
- if success:
- return jsonify({
- "success": True,
- "message": "已撤销所有客户端的独立状态"
- })
- else:
- return jsonify({
- "success": False,
- "message": "撤销所有客户端的独立状态失败"
- }), 500
- except Exception as e:
- logger.error(f"撤销独立状态异常: {str(e)}")
- return jsonify({
- "success": False,
- "message": f"撤销独立状态失败: {str(e)}"
- }), 500
- @app.route('/api/kodi/start_all_apps', methods=['POST'])
- def start_all_kodi_apps_api():
- """启动所有kodi应用程序接口"""
- try:
- success = start_all_kodi_apps()
- if success:
- return jsonify({
- "success": True,
- "message": "已启动所有kodi应用程序"
- })
- else:
- return jsonify({
- "success": False,
- "message": "启动所有kodi应用程序失败"
- }), 500
- except Exception as e:
- logger.error(f"启动所有kodi应用程序异常: {str(e)}")
- return jsonify({
- "success": False,
- "message": f"启动所有kodi应用程序失败: {str(e)}"
- }), 500
- # 提供上传文件的访问接口
- @app.route('/uploads/<filename>')
- def uploaded_file(filename):
- """提供上传文件的访问接口"""
- return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
- if __name__ == '__main__':
- # 创建templates目录
- import os
- if not os.path.exists('templates'):
- os.makedirs('templates')
- logger.info("启动Kodi心跳检测")
- start_kodi_alive_check()
- logger.info("启动Kodi空闲时间播放")
- start_kodi_free_time_play()
- logger.info("Flask API服务器启动中...")
- logger.info("访问 http://localhost:5050 查看HTML页面")
- logger.info("API端点:")
- logger.info(" GET /api/led/status - 获取LED状态")
- logger.info(" POST /api/led/start - 启动展品灯效")
- logger.info(" POST /api/led/stop - 停止灯效")
- logger.info(" GET /api/kodi/status - 获取Kodi状态")
- logger.info(" POST /api/kodi/start - 启动/切换Kodi播放")
- logger.info(" POST /api/kodi/play_image - 播放图片(支持上传或URL)")
- logger.info(" POST /api/kodi/play_rtsp - 播放RTSP视频流")
- logger.info(" POST /api/kodi/revoke_individual_state - 撤销所有客户端的独立状态")
- logger.info(" POST /api/kodi/start_all_apps - 启动所有kodi应用程序")
-
- app.run(debug=True, host='0.0.0.0', port=5050)
|