flask_api.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. from flask import Flask, jsonify, request, render_template, send_from_directory
  2. from flask_cors import CORS
  3. import json
  4. import os
  5. import time
  6. import uuid
  7. import socket
  8. from werkzeug.utils import secure_filename
  9. from application.kodi_alive_thread import start_kodi_alive_check
  10. from application.kodi_free_time_thread import start_kodi_free_time_play
  11. from application.wled_thread import start_exhibit_led_effect, stop_led_effect, is_effect_running
  12. 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
  13. from utils.logger_config import logger
  14. app = Flask(__name__)
  15. CORS(app) # 允许跨域请求
  16. # 配置上传文件夹
  17. UPLOAD_FOLDER = 'uploads'
  18. ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
  19. app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
  20. app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上传文件大小为16MB
  21. # 确保上传文件夹存在
  22. if not os.path.exists(UPLOAD_FOLDER):
  23. os.makedirs(UPLOAD_FOLDER)
  24. def allowed_file(filename):
  25. """检查文件扩展名是否允许"""
  26. return '.' in filename and \
  27. filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
  28. def get_server_ip():
  29. """获取服务器的本地IP地址"""
  30. try:
  31. # 连接到一个远程地址(不会实际发送数据)
  32. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  33. s.connect(("8.8.8.8", 80))
  34. ip = s.getsockname()[0]
  35. s.close()
  36. return ip
  37. except Exception:
  38. # 如果获取失败,尝试其他方法
  39. try:
  40. hostname = socket.gethostname()
  41. ip = socket.gethostbyname(hostname)
  42. return ip
  43. except Exception:
  44. return "127.0.0.1" # 回退到localhost
  45. @app.route('/')
  46. def index():
  47. """返回HTML页面"""
  48. return render_template('index.html')
  49. @app.route('/api/led/status', methods=['GET'])
  50. def get_led_status():
  51. """获取LED状态"""
  52. try:
  53. is_running = is_effect_running()
  54. return jsonify({
  55. "success": True,
  56. "data": {
  57. "is_running": is_running,
  58. "message": "灯效正在运行" if is_running else "灯效已停止"
  59. }
  60. })
  61. except Exception as e:
  62. return jsonify({
  63. "success": False,
  64. "message": f"获取状态失败: {str(e)}"
  65. }), 500
  66. @app.route('/api/led/start', methods=['POST'])
  67. def start_led_effect():
  68. """启动展品LED灯效控制"""
  69. try:
  70. data = request.get_json()
  71. if not data or 'exhibit_id' not in data:
  72. return jsonify({
  73. "success": False,
  74. "message": "缺少展品ID参数"
  75. }), 400
  76. exhibit_id = data['exhibit_id']
  77. if not isinstance(exhibit_id, int) or exhibit_id < 0:
  78. return jsonify({
  79. "success": False,
  80. "message": "展品ID必须是大于等于0的整数"
  81. }), 400
  82. success = start_exhibit_led_effect(exhibit_id)
  83. if success:
  84. return jsonify({
  85. "success": True,
  86. "message": f"展品 {exhibit_id} 的灯效已启动",
  87. "data": {
  88. "exhibit_id": exhibit_id,
  89. "is_running": True
  90. }
  91. })
  92. else:
  93. return jsonify({
  94. "success": False,
  95. "message": f"启动展品 {exhibit_id} 的灯效失败"
  96. }), 500
  97. except Exception as e:
  98. return jsonify({
  99. "success": False,
  100. "message": f"启动灯效失败: {str(e)}"
  101. }), 500
  102. @app.route('/api/led/stop', methods=['POST'])
  103. def stop_led_effect_api():
  104. """停止当前LED灯效"""
  105. try:
  106. success = stop_led_effect()
  107. if success:
  108. return jsonify({
  109. "success": True,
  110. "message": "灯效已停止",
  111. "data": {
  112. "is_running": False
  113. }
  114. })
  115. else:
  116. return jsonify({
  117. "success": False,
  118. "message": "停止灯效失败"
  119. }), 500
  120. except Exception as e:
  121. return jsonify({
  122. "success": False,
  123. "message": f"停止灯效失败: {str(e)}"
  124. }), 500
  125. # ===== Kodi 播放控制接口 =====
  126. @app.route('/api/kodi/status', methods=['GET'])
  127. def get_kodi_status():
  128. try:
  129. running = is_kodi_thread_running()
  130. return jsonify({
  131. "success": True,
  132. "data": {
  133. "is_running": running,
  134. "message": "Kodi播放线程运行中" if running else "Kodi播放线程已停止"
  135. }
  136. })
  137. except Exception as e:
  138. return jsonify({
  139. "success": False,
  140. "message": f"获取Kodi状态失败: {str(e)}"
  141. }), 500
  142. @app.route('/api/kodi/start', methods=['POST'])
  143. def start_kodi_play_api():
  144. try:
  145. data = request.get_json()
  146. if not data or 'video_id' not in data:
  147. return jsonify({
  148. "success": False,
  149. "message": "缺少视频ID参数"
  150. }), 400
  151. video_id = data['video_id']
  152. if not isinstance(video_id, int) or video_id < 0:
  153. return jsonify({
  154. "success": False,
  155. "message": "视频ID必须是大于等于0的整数"
  156. }), 400
  157. ok = start_kodi_play(video_id)
  158. if ok:
  159. return jsonify({
  160. "success": True,
  161. "message": f"Kodi开始/切换播放 视频ID={video_id}",
  162. "data": {"video_id": video_id}
  163. })
  164. return jsonify({
  165. "success": False,
  166. "message": f"Kodi播放启动失败(视频ID={video_id})"
  167. }), 500
  168. except Exception as e:
  169. return jsonify({
  170. "success": False,
  171. "message": f"Kodi播放启动异常: {str(e)}"
  172. }), 500
  173. # 指定某台kodi_client_index播放图片,这边要上传图片并且传递完整url给kodi播放
  174. @app.route('/api/kodi/play_image', methods=['POST'])
  175. def play_image_api():
  176. """播放图片接口,支持文件上传或直接传递图片URL"""
  177. try:
  178. # 检查是否有文件上传
  179. if 'file' in request.files:
  180. file = request.files['file']
  181. if file.filename == '':
  182. return jsonify({
  183. "success": False,
  184. "message": "未选择文件"
  185. }), 400
  186. if file and allowed_file(file.filename):
  187. # 获取原始文件扩展名
  188. original_filename = file.filename
  189. # 提取文件扩展名(不包含点号)
  190. if '.' in original_filename:
  191. file_ext = original_filename.rsplit('.', 1)[1].lower()
  192. else:
  193. # 如果没有扩展名,根据Content-Type推断或默认为jpg
  194. content_type = file.content_type or ''
  195. if 'png' in content_type:
  196. file_ext = 'png'
  197. elif 'jpeg' in content_type or 'jpg' in content_type:
  198. file_ext = 'jpg'
  199. elif 'gif' in content_type:
  200. file_ext = 'gif'
  201. else:
  202. file_ext = 'jpg' # 默认扩展名
  203. # 确保扩展名在允许列表中
  204. if file_ext not in ALLOWED_EXTENSIONS:
  205. file_ext = 'jpg'
  206. # 生成唯一文件名:使用时间戳和UUID,确保文件名唯一且安全
  207. timestamp = int(time.time() * 1000)
  208. unique_id = str(uuid.uuid4())[:8] # 使用UUID的前8位作为唯一标识
  209. filename = f"{timestamp}_{unique_id}.{file_ext}"
  210. filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  211. file.save(filepath)
  212. # 生成可访问的URL(使用服务器的实际IP地址,而不是localhost)
  213. # 如果request.host包含localhost或127.0.0.1,使用实际IP
  214. host = request.host
  215. if 'localhost' in host or '127.0.0.1' in host:
  216. server_ip = get_server_ip()
  217. port = request.environ.get('SERVER_PORT', '5000')
  218. host = f"{server_ip}:{port}"
  219. image_url = f"http://{host}/uploads/{filename}"
  220. logger.info(f"图片已上传: {filepath}, URL: {image_url}, 原始文件名: {original_filename}")
  221. else:
  222. return jsonify({
  223. "success": False,
  224. "message": f"不支持的文件类型,允许的类型: {', '.join(ALLOWED_EXTENSIONS)}"
  225. }), 400
  226. elif request.is_json:
  227. # 检查是否有直接的图片URL
  228. data = request.get_json()
  229. if 'image_url' not in data:
  230. return jsonify({
  231. "success": False,
  232. "message": "缺少参数:需要 'file'(文件上传)或 'image_url'(图片URL)"
  233. }), 400
  234. image_url = data['image_url']
  235. if not image_url or not isinstance(image_url, str):
  236. return jsonify({
  237. "success": False,
  238. "message": "无效的图片URL"
  239. }), 400
  240. else:
  241. return jsonify({
  242. "success": False,
  243. "message": "缺少参数:需要 'file'(文件上传)或 'image_url'(图片URL)"
  244. }), 400
  245. # 获取客户端索引
  246. if 'kodi_client_index' in request.form:
  247. try:
  248. client_index = int(request.form['kodi_client_index'])
  249. except (ValueError, TypeError):
  250. return jsonify({
  251. "success": False,
  252. "message": "kodi_client_index 必须是整数"
  253. }), 400
  254. elif request.is_json and 'kodi_client_index' in request.get_json():
  255. client_index = request.get_json()['kodi_client_index']
  256. else:
  257. return jsonify({
  258. "success": False,
  259. "message": "缺少参数: kodi_client_index"
  260. }), 400
  261. if not isinstance(client_index, int) or client_index < 0:
  262. return jsonify({
  263. "success": False,
  264. "message": "kodi_client_index 必须是大于等于0的整数"
  265. }), 400
  266. # 调用播放函数
  267. success = play_image(image_url, client_index)
  268. if success:
  269. return jsonify({
  270. "success": True,
  271. "message": f"已在客户端 {client_index} 上启动图片播放",
  272. "data": {
  273. "image_url": image_url,
  274. "client_index": client_index
  275. }
  276. })
  277. else:
  278. return jsonify({
  279. "success": False,
  280. "message": f"在客户端 {client_index} 上启动图片播放失败"
  281. }), 500
  282. except Exception as e:
  283. logger.error(f"播放图片异常: {str(e)}")
  284. return jsonify({
  285. "success": False,
  286. "message": f"播放图片失败: {str(e)}"
  287. }), 500
  288. # 指定某台kodi_client_index播放rtsp视频
  289. @app.route('/api/kodi/play_rtsp', methods=['POST'])
  290. def play_rtsp_api():
  291. """播放RTSP视频流接口"""
  292. try:
  293. data = request.get_json()
  294. if not data:
  295. return jsonify({
  296. "success": False,
  297. "message": "请求体不能为空"
  298. }), 400
  299. # 检查必需的参数
  300. if 'rtsp_url' not in data:
  301. return jsonify({
  302. "success": False,
  303. "message": "缺少参数: rtsp_url"
  304. }), 400
  305. if 'kodi_client_index' not in data:
  306. return jsonify({
  307. "success": False,
  308. "message": "缺少参数: kodi_client_index"
  309. }), 400
  310. rtsp_url = data['rtsp_url']
  311. client_index = data['kodi_client_index']
  312. volume = data.get('volume', 0) # 可选参数,默认为0
  313. # 参数验证
  314. if not isinstance(rtsp_url, str) or not rtsp_url.strip():
  315. return jsonify({
  316. "success": False,
  317. "message": "rtsp_url 必须是有效的字符串"
  318. }), 400
  319. if not isinstance(client_index, int) or client_index < 0:
  320. return jsonify({
  321. "success": False,
  322. "message": "kodi_client_index 必须是大于等于0的整数"
  323. }), 400
  324. if not isinstance(volume, int) or volume < 0 or volume > 100:
  325. return jsonify({
  326. "success": False,
  327. "message": "volume 必须是 0-100 之间的整数"
  328. }), 400
  329. # 调用播放函数
  330. success = play_rtsp(rtsp_url, client_index, volume)
  331. if success:
  332. return jsonify({
  333. "success": True,
  334. "message": f"已在客户端 {client_index} 上启动RTSP播放",
  335. "data": {
  336. "rtsp_url": rtsp_url,
  337. "client_index": client_index,
  338. "volume": volume
  339. }
  340. })
  341. else:
  342. return jsonify({
  343. "success": False,
  344. "message": f"在客户端 {client_index} 上启动RTSP播放失败"
  345. }), 500
  346. except Exception as e:
  347. logger.error(f"播放RTSP异常: {str(e)}")
  348. return jsonify({
  349. "success": False,
  350. "message": f"播放RTSP失败: {str(e)}"
  351. }), 500
  352. @app.route('/api/kodi/revoke_individual_state', methods=['POST'])
  353. def revoke_individual_state_api():
  354. """撤销所有客户端的独立状态接口"""
  355. try:
  356. success = revoke_individual_state()
  357. if success:
  358. return jsonify({
  359. "success": True,
  360. "message": "已撤销所有客户端的独立状态"
  361. })
  362. else:
  363. return jsonify({
  364. "success": False,
  365. "message": "撤销所有客户端的独立状态失败"
  366. }), 500
  367. except Exception as e:
  368. logger.error(f"撤销独立状态异常: {str(e)}")
  369. return jsonify({
  370. "success": False,
  371. "message": f"撤销独立状态失败: {str(e)}"
  372. }), 500
  373. @app.route('/api/kodi/start_all_apps', methods=['POST'])
  374. def start_all_kodi_apps_api():
  375. """启动所有kodi应用程序接口"""
  376. try:
  377. success = start_all_kodi_apps()
  378. if success:
  379. return jsonify({
  380. "success": True,
  381. "message": "已启动所有kodi应用程序"
  382. })
  383. else:
  384. return jsonify({
  385. "success": False,
  386. "message": "启动所有kodi应用程序失败"
  387. }), 500
  388. except Exception as e:
  389. logger.error(f"启动所有kodi应用程序异常: {str(e)}")
  390. return jsonify({
  391. "success": False,
  392. "message": f"启动所有kodi应用程序失败: {str(e)}"
  393. }), 500
  394. # 提供上传文件的访问接口
  395. @app.route('/uploads/<filename>')
  396. def uploaded_file(filename):
  397. """提供上传文件的访问接口"""
  398. return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
  399. if __name__ == '__main__':
  400. # 创建templates目录
  401. import os
  402. if not os.path.exists('templates'):
  403. os.makedirs('templates')
  404. logger.info("启动Kodi心跳检测")
  405. start_kodi_alive_check()
  406. logger.info("启动Kodi空闲时间播放")
  407. start_kodi_free_time_play()
  408. logger.info("Flask API服务器启动中...")
  409. logger.info("访问 http://localhost:5050 查看HTML页面")
  410. logger.info("API端点:")
  411. logger.info(" GET /api/led/status - 获取LED状态")
  412. logger.info(" POST /api/led/start - 启动展品灯效")
  413. logger.info(" POST /api/led/stop - 停止灯效")
  414. logger.info(" GET /api/kodi/status - 获取Kodi状态")
  415. logger.info(" POST /api/kodi/start - 启动/切换Kodi播放")
  416. logger.info(" POST /api/kodi/play_image - 播放图片(支持上传或URL)")
  417. logger.info(" POST /api/kodi/play_rtsp - 播放RTSP视频流")
  418. logger.info(" POST /api/kodi/revoke_individual_state - 撤销所有客户端的独立状态")
  419. logger.info(" POST /api/kodi/start_all_apps - 启动所有kodi应用程序")
  420. app.run(debug=True, host='0.0.0.0', port=5050)