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/') 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)