kodi_server.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import json
  2. import requests
  3. import time as time_module
  4. import threading
  5. import yaml
  6. import os
  7. from kodi_util.kodi_module import KodiClient
  8. from kodi_util.kodi_play.thread import PlaybackMonitorThread, KodiStateMonitorThread
  9. class KodiServer:
  10. def __init__(self, config_path="config/config.yaml"):
  11. """初始化 KodiServer
  12. Args:
  13. config_path: 配置文件路径,默认为 config/config.yaml
  14. """
  15. self.clients = []
  16. self.state_monitor = None
  17. # 读取配置文件
  18. if not os.path.exists(config_path):
  19. raise FileNotFoundError(f"配置文件 {config_path} 不存在")
  20. with open(config_path, 'r', encoding='utf-8') as f:
  21. config = yaml.safe_load(f)
  22. # 从配置创建客户端
  23. if 'kodi_clients' not in config:
  24. raise ValueError("配置文件中缺少 kodi_clients 配置")
  25. for client_config in config['kodi_clients']:
  26. try:
  27. # 确保使用正确的参数名
  28. client = KodiClient(
  29. host=client_config.get('ip', 'localhost'), # 使用ip字段作为host参数
  30. port=client_config.get('port', 8080),
  31. username=client_config.get('username'),
  32. password=client_config.get('password')
  33. )
  34. print(f"创建客户端: host={client.host}, port={client.port}") # 添加调试信息
  35. self.clients.append(client)
  36. except Exception as e:
  37. print(f"创建客户端失败 {client_config.get('ip', 'unknown')}: {str(e)}")
  38. if not self.clients:
  39. print("警告:没有成功创建任何客户端")
  40. # 如果状态监控线程不存在,创建并启动它
  41. if self.state_monitor is None:
  42. self.state_monitor = KodiStateMonitorThread(self.clients)
  43. self.state_monitor.start()
  44. else:
  45. # 如果已存在,更新客户端列表
  46. self.state_monitor.update_clients(self.clients)
  47. def add_client(self, client):
  48. """
  49. 添加Kodi客户端
  50. Args:
  51. client: KodiClient实例
  52. """
  53. self.clients.append(client)
  54. # 如果状态监控线程不存在,创建并启动它
  55. if self.state_monitor is None:
  56. self.state_monitor = KodiStateMonitorThread(self.clients)
  57. self.state_monitor.start()
  58. else:
  59. # 如果已存在,更新客户端列表
  60. self.state_monitor.update_clients(self.clients)
  61. def remove_client(self, client):
  62. """
  63. 移除Kodi客户端
  64. Args:
  65. client: KodiClient实例
  66. """
  67. if client in self.clients:
  68. self.clients.remove(client)
  69. if self.state_monitor:
  70. self.state_monitor.update_clients(self.clients)
  71. def sync_play_images(self, image_paths, effect="fade", time=5):
  72. """同步播放幻灯片
  73. Args:
  74. image_paths: 图片路径列表
  75. effect: 过渡效果
  76. time: 每张图片显示时间(秒)
  77. Returns:
  78. bool: 是否成功启动同步播放
  79. """
  80. if not self.clients:
  81. print("错误:没有连接的客户端")
  82. return False
  83. # 处理单个图片路径的情况
  84. if isinstance(image_paths, str):
  85. image_paths = [image_paths]
  86. # 创建一个共享的Event来控制所有客户端同时开始播放
  87. start_event = threading.Event()
  88. # 用于跟踪成功和失败的客户端
  89. ready_clients = []
  90. failed_clients = []
  91. def play_thread(client):
  92. try:
  93. # 准备阶段
  94. client.ready_event.clear()
  95. print(f"客户端 {client.url} 准备中...")
  96. # 先停止当前播放
  97. client.stop_playback()
  98. time_module.sleep(0.5) # 给一点时间确保停止完成
  99. # 标记客户端准备好了
  100. client.set_ready()
  101. # 等待全局开始信号
  102. start_event.wait()
  103. # 播放图片
  104. result = False
  105. if len(image_paths) == 1:
  106. # 只有一张图片,直接播放
  107. print(f"只有一张图片,客户端 {client.url} 直接播放")
  108. result = client.play_image(image_paths[0])
  109. else:
  110. # 多张图片使用幻灯片效果
  111. # 设置效果
  112. client.set_slideshow_settings(effect=effect, time=time)
  113. # 播放图片
  114. result = client.play_image_loop(image_paths)
  115. if result:
  116. print(f"客户端 {client.url} 播放成功")
  117. ready_clients.append(client)
  118. return True
  119. else:
  120. print(f"客户端 {client.url} 播放失败")
  121. failed_clients.append(client)
  122. return False
  123. except Exception as e:
  124. print(f"客户端 {client.url} 播放出错: {str(e)}")
  125. failed_clients.append(client)
  126. return False
  127. # 启动所有播放线程
  128. threads = []
  129. for client in self.clients:
  130. thread = threading.Thread(target=play_thread, args=(client,))
  131. thread.daemon = True # 设置为守护线程
  132. threads.append(thread)
  133. thread.start()
  134. # 等待所有客户端准备就绪
  135. all_ready = True
  136. for client in self.clients:
  137. if not client.wait_for_ready(timeout=10):
  138. print(f"客户端 {client.url} 未能在超时时间内准备就绪")
  139. all_ready = False
  140. if not all_ready:
  141. print("警告:部分客户端未准备就绪,但仍将继续尝试同步播放")
  142. # 同时触发所有客户端开始播放
  143. print("所有客户端准备就绪,开始同步播放...")
  144. start_event.set()
  145. # 等待所有线程完成
  146. for thread in threads:
  147. thread.join(timeout=15) # 设置超时时间,避免无限等待
  148. # 报告播放状态
  149. if failed_clients:
  150. print(f"警告:{len(failed_clients)} 个客户端播放失败,{len(ready_clients)} 个客户端播放成功")
  151. # 只要有一个客户端成功播放,就认为同步播放成功
  152. return len(ready_clients) > 0
  153. def sync_play_video(self, video_path, start_time=0, end_time=None, sound_client_index=0, default_volume=100, loop=False, client_loop_settings=None):
  154. """
  155. 同步播放视频 - 并发下发版本:为所有客户端并发创建播放线程
  156. Args:
  157. video_path: 视频路径
  158. start_time: 开始时间(秒)
  159. end_time: 结束时间(秒)
  160. sound_client_index: 播放声音的客户端索引,默认为0(第一台设备)
  161. 如果为-1,则所有客户端都播放声音
  162. default_volume: 播放声音的客户端的默认音量(0-100)
  163. loop: 是否循环播放视频,如果提供了client_loop_settings则此参数作为默认值
  164. client_loop_settings: 每个客户端的循环播放设置列表,与clients列表长度相同
  165. Returns:
  166. dict: 播放结果
  167. """
  168. if not self.clients:
  169. return {"success": False, "message": "没有可用的客户端"}
  170. # 验证声音客户端索引是否有效
  171. master_client_index = sound_client_index
  172. if master_client_index >= len(self.clients) and master_client_index != -1:
  173. print(f"警告:指定的声音客户端索引 {master_client_index} 超出范围,将使用默认客户端")
  174. master_client_index = 0
  175. # 如果为-1,设置为第一个客户端作为主客户端
  176. if master_client_index == -1 and len(self.clients) > 0:
  177. master_client_index = 0
  178. print(f"声音客户端索引为-1,使用第一个客户端(索引0)作为主客户端")
  179. # 验证client_loop_settings是否有效
  180. if client_loop_settings is not None:
  181. if len(client_loop_settings) != len(self.clients):
  182. print(f"警告:循环设置列表长度({len(client_loop_settings)})与客户端数量({len(self.clients)})不匹配,将使用默认设置")
  183. client_loop_settings = None
  184. # 创建同步事件
  185. start_event = threading.Event()
  186. # 用于跟踪成功和失败的客户端
  187. success_clients = []
  188. failed_clients = []
  189. def play_thread(client, idx):
  190. """每个客户端的播放线程"""
  191. try:
  192. # 准备阶段
  193. client.ready_event.clear()
  194. print(f"客户端 {client.host} 准备中...")
  195. # 先停止当前播放
  196. client.stop_playback()
  197. time_module.sleep(0.2) # 短暂等待确保停止完成
  198. # 设置音量
  199. if idx == master_client_index or sound_client_index == -1:
  200. print(f"客户端 {client.host} 将播放声音,音量设置为 {default_volume}%")
  201. client.set_volume(default_volume)
  202. else:
  203. print(f"客户端 {client.host} 将静音播放")
  204. client.set_volume(0)
  205. # 获取此客户端的循环设置
  206. client_loop = loop # 默认使用传入的loop参数
  207. if client_loop_settings is not None and idx < len(client_loop_settings):
  208. client_loop = client_loop_settings[idx]
  209. # 标记客户端准备好了
  210. client.set_ready()
  211. # 等待全局开始信号
  212. start_event.wait()
  213. # 播放视频
  214. print(f"客户端 {client.host} 开始播放,循环设置: {client_loop}")
  215. result = client.play_video(video_path, loop=client_loop)
  216. if result and result.get('result') == 'OK':
  217. print(f"客户端 {client.host} 播放成功")
  218. success_clients.append(client)
  219. return True
  220. else:
  221. print(f"客户端 {client.host} 播放失败: {result}")
  222. failed_clients.append(client)
  223. return False
  224. except Exception as e:
  225. print(f"客户端 {client.host} 播放出错: {str(e)}")
  226. failed_clients.append(client)
  227. return False
  228. # 启动所有播放线程
  229. threads = []
  230. for idx, client in enumerate(self.clients):
  231. thread = threading.Thread(target=play_thread, args=(client, idx))
  232. thread.daemon = True # 设置为守护线程
  233. threads.append(thread)
  234. thread.start()
  235. # 等待所有客户端准备就绪
  236. all_ready = True
  237. for client in self.clients:
  238. if not client.wait_for_ready(timeout=10):
  239. print(f"客户端 {client.host} 未能在超时时间内准备就绪")
  240. all_ready = False
  241. if not all_ready:
  242. print("警告:部分客户端未准备就绪,但仍将继续尝试同步播放")
  243. # 同时触发所有客户端开始播放
  244. print("所有客户端准备就绪,开始同步播放...")
  245. start_event.set()
  246. # 等待所有线程完成
  247. for thread in threads:
  248. thread.join(timeout=15) # 设置超时时间,避免无限等待
  249. # 返回播放结果
  250. if not success_clients:
  251. return {
  252. "success": False,
  253. "message": "所有客户端播放失败",
  254. "failed_clients": [c.host for c in failed_clients]
  255. }
  256. return {
  257. "success": True,
  258. "success_clients": [c.host for c in success_clients],
  259. "failed_clients": [c.host for c in failed_clients]
  260. }
  261. def stop_playback(self):
  262. """
  263. 停止所有客户端的播放
  264. Returns:
  265. dict: 停止结果
  266. """
  267. if not self.clients:
  268. return {"success": False, "message": "没有可用的客户端"}
  269. success_clients = []
  270. failed_clients = []
  271. for client in self.clients:
  272. try:
  273. client.stop_playback()
  274. success_clients.append(client)
  275. except Exception as e:
  276. print(f"停止客户端 {client.host} 播放时出错: {str(e)}")
  277. failed_clients.append(client)
  278. return {
  279. "success": len(success_clients) > 0,
  280. "success_clients": [c.host for c in success_clients],
  281. "failed_clients": [c.host for c in failed_clients]
  282. }
  283. def get_client_states(self):
  284. """
  285. 获取所有客户端的状态
  286. Returns:
  287. dict: 客户端状态字典
  288. """
  289. if not self.state_monitor:
  290. return {}
  291. return self.state_monitor.get_all_states()
  292. def get_client_state(self, client_index):
  293. """
  294. 获取指定客户端的状态
  295. Args:
  296. client_index: 客户端索引
  297. Returns:
  298. dict: 客户端状态
  299. """
  300. if not self.state_monitor:
  301. return {}
  302. return self.state_monitor.get_state(client_index)
  303. def is_client_playing(self, client_index):
  304. """
  305. 检查指定客户端是否正在播放
  306. Args:
  307. client_index: 客户端索引
  308. Returns:
  309. bool: 是否正在播放
  310. """
  311. if not self.state_monitor:
  312. return False
  313. return self.state_monitor.is_client_playing(client_index)
  314. def get_client_playback_time(self, client_index):
  315. """
  316. 获取指定客户端的播放时间
  317. Args:
  318. client_index: 客户端索引
  319. Returns:
  320. float: 播放时间(秒)
  321. """
  322. if not self.state_monitor:
  323. return 0.0
  324. return self.state_monitor.get_playback_time(client_index)
  325. def get_client_volume(self, client_index):
  326. """
  327. 获取指定客户端的音量
  328. Args:
  329. client_index: 客户端索引
  330. Returns:
  331. int: 音量值
  332. """
  333. if not self.state_monitor:
  334. return 0
  335. return self.state_monitor.get_volume(client_index)