kodi.py 16 KB

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