kodi.py 17 KB

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