debug_module_window.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import sys
  4. import os
  5. # 添加项目根目录到 Python 路径
  6. current_dir = os.path.dirname(os.path.abspath(__file__))
  7. project_root = os.path.dirname(current_dir)
  8. sys.path.append(project_root)
  9. import yaml
  10. import time
  11. import threading
  12. from PyQt6.QtWidgets import (
  13. QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  14. QPushButton, QLabel, QFrame, QFileDialog, QGroupBox, QScrollArea,
  15. QSlider, QSpinBox, QComboBox
  16. )
  17. from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal, QObject
  18. from PyQt6.QtGui import QFont
  19. from kodi_util.kodi_module import KodiClient
  20. from kodi_util.kodi_play.thread import KodiStateMonitorThread
  21. class ClientWidget(QWidget):
  22. """单个客户端控制窗口"""
  23. def __init__(self, client: KodiClient, client_idx: int, state_monitor: KodiStateMonitorThread, title: str, config_path: str, parent=None):
  24. super().__init__(parent)
  25. self.client = client
  26. self.client_idx = client_idx # 客户端在列表中的索引
  27. self.state_monitor = state_monitor # 状态监控线程引用
  28. self.title = title
  29. self.config_path = config_path # 保存配置文件路径
  30. self.current_video_path = None # 当前播放的视频路径
  31. # 创建定时器用于UI更新
  32. self.ui_update_timer = QTimer(self)
  33. self.ui_update_timer.timeout.connect(self.update_ui_from_status)
  34. self.ui_update_timer.start(1000) # 每1秒更新一次UI
  35. # 创建音量调节去抖动定时器
  36. self.volume_timer = QTimer(self)
  37. self.volume_timer.setSingleShot(True)
  38. self.volume_timer.timeout.connect(self.apply_volume_change)
  39. self.pending_volume = None
  40. # 加载视频列表
  41. self.video_paths = self.load_video_paths()
  42. self.init_ui()
  43. def load_video_paths(self):
  44. """从配置文件加载视频路径列表"""
  45. try:
  46. with open(self.config_path, 'r', encoding='utf-8') as f:
  47. config = yaml.safe_load(f)
  48. return config.get('video_paths', [])
  49. except Exception as e:
  50. print(f"加载视频路径列表失败: {str(e)}")
  51. return []
  52. def init_ui(self):
  53. """初始化客户端窗口UI"""
  54. layout = QVBoxLayout(self)
  55. # 创建分组框
  56. group_box = QGroupBox(self.title)
  57. group_layout = QVBoxLayout()
  58. # 状态显示区域
  59. status_frame = QFrame()
  60. status_frame.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken)
  61. status_layout = QVBoxLayout(status_frame)
  62. # 连接状态
  63. self.connection_label = QLabel("连接状态: 检查中...")
  64. self.connection_label.setStyleSheet("QLabel { color: gray; }")
  65. status_layout.addWidget(self.connection_label)
  66. # 播放状态
  67. self.playback_label = QLabel("播放状态: 未播放")
  68. self.playback_label.setStyleSheet("QLabel { color: gray; }")
  69. status_layout.addWidget(self.playback_label)
  70. # 地址显示
  71. address_label = QLabel(f"地址: {self.client.url}")
  72. status_layout.addWidget(address_label)
  73. group_layout.addWidget(status_frame)
  74. # 音量控制区域
  75. volume_frame = QFrame()
  76. volume_frame.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken)
  77. volume_layout = QHBoxLayout(volume_frame)
  78. # 音量标签
  79. volume_label = QLabel("音量:")
  80. volume_layout.addWidget(volume_label)
  81. # 音量滑块
  82. self.volume_slider = QSlider(Qt.Orientation.Horizontal)
  83. self.volume_slider.setMinimum(0)
  84. self.volume_slider.setMaximum(100)
  85. self.volume_slider.setValue(50) # 默认音量50%
  86. self.volume_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
  87. self.volume_slider.setTickInterval(10)
  88. self.volume_slider.valueChanged.connect(self.on_volume_changed)
  89. volume_layout.addWidget(self.volume_slider)
  90. # 音量数值显示
  91. self.volume_spinbox = QSpinBox()
  92. self.volume_spinbox.setMinimum(0)
  93. self.volume_spinbox.setMaximum(100)
  94. self.volume_spinbox.setValue(50)
  95. self.volume_spinbox.valueChanged.connect(self.volume_slider.setValue)
  96. volume_layout.addWidget(self.volume_spinbox)
  97. # 音量百分比标签
  98. self.volume_percent_label = QLabel("50%")
  99. volume_layout.addWidget(self.volume_percent_label)
  100. group_layout.addWidget(volume_frame)
  101. # 视频控制区域
  102. video_control_frame = QFrame()
  103. video_control_frame.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken)
  104. video_control_layout = QHBoxLayout(video_control_frame)
  105. # 视频选择下拉列表
  106. self.video_combo = QComboBox()
  107. self.video_combo.addItems([os.path.basename(path) for path in self.video_paths])
  108. self.video_combo.setMinimumWidth(200)
  109. video_control_layout.addWidget(self.video_combo)
  110. # 播放/暂停按钮
  111. self.play_pause_btn = QPushButton("播放")
  112. self.play_pause_btn.setEnabled(False)
  113. self.play_pause_btn.clicked.connect(self.toggle_play_pause)
  114. video_control_layout.addWidget(self.play_pause_btn)
  115. # 停止按钮
  116. self.stop_btn = QPushButton("停止")
  117. self.stop_btn.setEnabled(False)
  118. self.stop_btn.clicked.connect(self.stop_playback)
  119. video_control_layout.addWidget(self.stop_btn)
  120. # 当前播放文件名标签
  121. self.current_file_label = QLabel("当前文件: 无")
  122. video_control_layout.addWidget(self.current_file_label)
  123. group_layout.addWidget(video_control_frame)
  124. # 按钮布局
  125. button_layout = QHBoxLayout()
  126. # 播放视频按钮
  127. self.video_btn = QPushButton("播放视频")
  128. self.video_btn.clicked.connect(self.play_video)
  129. button_layout.addWidget(self.video_btn)
  130. # 播放图片按钮
  131. self.image_btn = QPushButton("播放图片")
  132. self.image_btn.clicked.connect(self.play_image)
  133. button_layout.addWidget(self.image_btn)
  134. group_layout.addLayout(button_layout)
  135. group_box.setLayout(group_layout)
  136. layout.addWidget(group_box)
  137. def toggle_play_pause(self):
  138. """切换播放/暂停状态"""
  139. try:
  140. # 获取当前播放器状态
  141. state = self.state_monitor.get_state(self.client_idx)
  142. is_playing = state.get('playing', False)
  143. if not is_playing:
  144. # 如果没有在播放,尝试播放当前选中的视频
  145. selected_index = self.video_combo.currentIndex()
  146. if selected_index >= 0 and selected_index < len(self.video_paths):
  147. video_path = self.video_paths[selected_index]
  148. self.client.play_video(video_path)
  149. self.current_video_path = video_path
  150. self.current_file_label.setText(f"当前文件: {os.path.basename(video_path)}")
  151. else:
  152. self.playback_label.setText("播放状态: 请先选择要播放的视频")
  153. self.playback_label.setStyleSheet("QLabel { color: red; }")
  154. return
  155. else:
  156. # 如果正在播放,尝试暂停/继续
  157. self.client.pause_playback()
  158. except Exception as e:
  159. print(f"切换播放/暂停状态失败: {str(e)}")
  160. self.playback_label.setText(f"播放状态: 操作失败 - {str(e)}")
  161. self.playback_label.setStyleSheet("QLabel { color: red; }")
  162. def stop_playback(self):
  163. """停止播放"""
  164. try:
  165. self.client.stop_playback()
  166. self.current_video_path = None
  167. self.current_file_label.setText("当前文件: 无")
  168. self.play_pause_btn.setEnabled(False)
  169. self.stop_btn.setEnabled(False)
  170. self.play_pause_btn.setText("播放")
  171. except Exception as e:
  172. print(f"停止播放失败: {str(e)}")
  173. def on_volume_changed(self, value):
  174. """音量滑块值改变时的处理函数"""
  175. self.volume_spinbox.setValue(value)
  176. self.volume_percent_label.setText(f"{value}%")
  177. # 存储待更新的音量值
  178. self.pending_volume = value
  179. # 重置定时器,300毫秒后应用音量变化
  180. self.volume_timer.start(300)
  181. def apply_volume_change(self):
  182. """实际应用音量变化的函数"""
  183. if self.pending_volume is not None:
  184. try:
  185. self.client.set_volume(self.pending_volume)
  186. self.pending_volume = None
  187. except Exception as e:
  188. print(f"设置音量失败: {str(e)}")
  189. def update_ui_from_status(self):
  190. """从状态监控线程获取状态数据并更新UI"""
  191. # 获取客户端状态
  192. state = self.state_monitor.get_state(self.client_idx)
  193. # 更新连接状态UI
  194. is_connected = state.get('connected', False)
  195. if is_connected:
  196. self.connection_label.setText("连接状态: 已连接")
  197. self.connection_label.setStyleSheet("QLabel { color: green; }")
  198. self.video_btn.setEnabled(True)
  199. self.image_btn.setEnabled(True)
  200. self.volume_slider.setEnabled(True)
  201. self.volume_spinbox.setEnabled(True)
  202. self.play_pause_btn.setEnabled(True)
  203. self.stop_btn.setEnabled(True)
  204. # 更新音量显示
  205. volume = state.get('volume', 50)
  206. self.volume_slider.setValue(volume)
  207. self.volume_spinbox.setValue(volume)
  208. self.volume_percent_label.setText(f"{volume}%")
  209. else:
  210. self.connection_label.setText("连接状态: 未连接")
  211. self.connection_label.setStyleSheet("QLabel { color: red; }")
  212. self.video_btn.setEnabled(False)
  213. self.image_btn.setEnabled(False)
  214. self.volume_slider.setEnabled(False)
  215. self.volume_spinbox.setEnabled(False)
  216. self.play_pause_btn.setEnabled(False)
  217. self.stop_btn.setEnabled(False)
  218. self.playback_label.setText("播放状态: 未连接")
  219. self.playback_label.setStyleSheet("QLabel { color: gray; }")
  220. return
  221. # 更新播放状态UI
  222. is_playing = state.get('playing', False)
  223. is_paused = state.get('paused', False)
  224. if is_playing:
  225. current_time = self._format_time(state.get('time', 0))
  226. total_time = self._format_time(state.get('totaltime', 0))
  227. status_text = "暂停" if is_paused else "正在播放"
  228. self.playback_label.setText(f"播放状态: {status_text} ({current_time}/{total_time})")
  229. self.playback_label.setStyleSheet("QLabel { color: blue; }")
  230. self.play_pause_btn.setText("暂停" if not is_paused else "播放")
  231. # 更新当前文件显示
  232. current_file = state.get('current_file', '')
  233. if current_file:
  234. self.current_file_label.setText(f"当前文件: {os.path.basename(current_file)}")
  235. else:
  236. self.play_pause_btn.setText("播放")
  237. self.playback_label.setText("播放状态: 未播放")
  238. self.playback_label.setStyleSheet("QLabel { color: gray; }")
  239. def _format_time(self, seconds):
  240. """将秒数格式化为时:分:秒格式"""
  241. hours = seconds // 3600
  242. minutes = (seconds % 3600) // 60
  243. seconds = seconds % 60
  244. if hours > 0:
  245. return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
  246. return f"{minutes:02d}:{seconds:02d}"
  247. def play_video(self):
  248. """播放视频按钮点击处理"""
  249. selected_index = self.video_combo.currentIndex()
  250. if selected_index >= 0 and selected_index < len(self.video_paths):
  251. video_path = self.video_paths[selected_index]
  252. try:
  253. self.client.play_video(video_path)
  254. self.current_video_path = video_path
  255. self.current_file_label.setText(f"当前文件: {os.path.basename(video_path)}")
  256. self.play_pause_btn.setEnabled(True)
  257. self.stop_btn.setEnabled(True)
  258. except Exception as e:
  259. self.playback_label.setText(f"播放状态: 播放失败 - {str(e)}")
  260. self.playback_label.setStyleSheet("QLabel { color: red; }")
  261. def play_image(self):
  262. """播放图片按钮点击处理"""
  263. file_path, _ = QFileDialog.getOpenFileName(
  264. self,
  265. "选择图片文件",
  266. "",
  267. "图片文件 (*.jpg *.jpeg *.png *.bmp);;所有文件 (*.*)"
  268. )
  269. if file_path:
  270. try:
  271. self.client.play_image(file_path)
  272. self.current_file_label.setText(f"当前文件: {os.path.basename(file_path)}")
  273. except Exception as e:
  274. self.playback_label.setText(f"播放状态: 显示失败 - {str(e)}")
  275. self.playback_label.setStyleSheet("QLabel { color: red; }")
  276. def closeEvent(self, event):
  277. """窗口关闭时停止定时器"""
  278. self.ui_update_timer.stop()
  279. super().closeEvent(event)
  280. def update_video_list(self):
  281. """更新视频列表"""
  282. # 重新加载视频路径
  283. self.video_paths = self.load_video_paths()
  284. # 保存当前选中的视频
  285. current_text = self.video_combo.currentText()
  286. # 清空并重新添加视频列表
  287. self.video_combo.clear()
  288. self.video_combo.addItems([os.path.basename(path) for path in self.video_paths])
  289. # 尝试恢复之前选中的视频
  290. index = self.video_combo.findText(current_text)
  291. if index >= 0:
  292. self.video_combo.setCurrentIndex(index)
  293. class DebugWindow(QMainWindow):
  294. """播放控制主窗口"""
  295. def __init__(self, config_path=None):
  296. super().__init__()
  297. # 设置配置文件路径
  298. self.config_path = config_path or os.path.join("config", "config.yaml")
  299. # 设置窗口标题和大小
  300. self.setWindowTitle("调试模块")
  301. self.setMinimumSize(QSize(800, 600))
  302. # 创建中央部件
  303. self.central_widget = QWidget()
  304. self.setCentralWidget(self.central_widget)
  305. # 创建主布局
  306. self.main_layout = QVBoxLayout(self.central_widget)
  307. self.main_layout.setContentsMargins(10, 10, 10, 10)
  308. self.main_layout.setSpacing(10)
  309. # 创建配置文件选择区域
  310. config_layout = QHBoxLayout()
  311. # 配置文件路径显示
  312. self.config_label = QLabel(f"当前配置文件: {self.config_path}")
  313. self.config_label.setFont(QFont("Arial", 10))
  314. config_layout.addWidget(self.config_label)
  315. # 选择配置文件按钮
  316. select_config_btn = QPushButton("选择配置文件")
  317. select_config_btn.clicked.connect(self.select_config_file)
  318. config_layout.addWidget(select_config_btn)
  319. # 重新加载按钮
  320. reload_btn = QPushButton("重新加载")
  321. reload_btn.clicked.connect(self.reload_config)
  322. config_layout.addWidget(reload_btn)
  323. self.main_layout.addLayout(config_layout)
  324. # 创建滚动区域
  325. self.scroll_area = QScrollArea()
  326. self.scroll_area.setWidgetResizable(True)
  327. self.scroll_widget = QWidget()
  328. self.scroll_layout = QVBoxLayout(self.scroll_widget)
  329. self.scroll_area.setWidget(self.scroll_widget)
  330. self.main_layout.addWidget(self.scroll_area)
  331. # 客户端列表
  332. self.clients = []
  333. # 初始化UI
  334. self.init_ui()
  335. def select_config_file(self):
  336. """选择配置文件"""
  337. file_path, _ = QFileDialog.getOpenFileName(
  338. self,
  339. "选择配置文件",
  340. "",
  341. "YAML文件 (*.yaml *.yml);;所有文件 (*.*)"
  342. )
  343. if file_path:
  344. self.config_path = file_path
  345. self.config_label.setText(f"当前配置文件: {self.config_path}")
  346. self.reload_config()
  347. def reload_config(self):
  348. """重新加载配置"""
  349. # 清除现有的客户端窗口
  350. while self.scroll_layout.count():
  351. item = self.scroll_layout.takeAt(0)
  352. if item.widget():
  353. item.widget().deleteLater()
  354. # 清空客户端列表
  355. self.clients = []
  356. # 停止状态监控线程
  357. if hasattr(self, 'state_monitor'):
  358. self.state_monitor.stop()
  359. # 重新初始化UI
  360. self.init_ui()
  361. # 更新所有客户端的视频列表
  362. for i in range(self.scroll_layout.count()):
  363. widget = self.scroll_layout.itemAt(i).widget()
  364. if isinstance(widget, ClientWidget):
  365. widget.update_video_list()
  366. def load_config(self):
  367. """从配置文件加载客户端配置
  368. Returns:
  369. list: 客户端配置列表,如果加载失败则返回默认配置
  370. """
  371. # 默认配置
  372. default_config = {
  373. 'kodi_clients': [
  374. {
  375. 'ip': 'localhost',
  376. 'port': 8080,
  377. 'username': 'kodi',
  378. 'password': '123'
  379. }
  380. ]
  381. }
  382. try:
  383. # 检查配置文件是否存在
  384. if not os.path.exists(self.config_path):
  385. print(f"警告:配置文件 {self.config_path} 不存在,使用默认配置")
  386. return default_config['kodi_clients']
  387. # 读取配置文件
  388. with open(self.config_path, 'r', encoding='utf-8') as f:
  389. config = yaml.safe_load(f)
  390. # 验证配置文件格式
  391. if not config or 'kodi_clients' not in config:
  392. print(f"警告:配置文件 {self.config_path} 格式错误,使用默认配置")
  393. return default_config['kodi_clients']
  394. return config['kodi_clients']
  395. except Exception as e:
  396. print(f"加载配置文件失败: {str(e)},使用默认配置")
  397. return default_config['kodi_clients']
  398. def init_ui(self):
  399. """初始化用户界面"""
  400. # 从配置文件加载客户端信息
  401. clients_config = self.load_config()
  402. if not clients_config:
  403. # 如果没有找到配置,显示错误信息
  404. error_label = QLabel("错误:未能加载客户端配置")
  405. error_label.setStyleSheet("QLabel { color: red; }")
  406. self.scroll_layout.addWidget(error_label)
  407. return
  408. # 创建客户端列表
  409. for client_config in clients_config:
  410. try:
  411. # 创建客户端实例,确保使用正确的参数名
  412. client = KodiClient(
  413. host=client_config.get('ip', 'localhost'), # 使用ip字段作为host参数
  414. port=client_config.get('port', 8080),
  415. username=client_config.get('username'),
  416. password=client_config.get('password')
  417. )
  418. print(f"创建客户端: host={client.host}, port={client.port}") # 添加调试信息
  419. self.clients.append(client)
  420. except Exception as e:
  421. print(f"创建客户端失败: {str(e)}")
  422. # 创建并启动状态监控线程
  423. self.state_monitor = KodiStateMonitorThread(self.clients)
  424. self.state_monitor.start()
  425. # 为每个客户端创建控制窗口
  426. for i, client in enumerate(self.clients):
  427. try:
  428. # 创建客户端窗口,传入配置文件路径
  429. client_widget = ClientWidget(client, i, self.state_monitor, f"客户端 {i+1}", self.config_path)
  430. self.scroll_layout.addWidget(client_widget)
  431. except Exception as e:
  432. # 如果创建客户端窗口失败,显示错误信息
  433. error_label = QLabel(f"客户端 {i+1} 窗口创建失败: {str(e)}")
  434. error_label.setStyleSheet("QLabel { color: red; }")
  435. self.scroll_layout.addWidget(error_label)
  436. # 添加一个弹性空间
  437. self.scroll_layout.addStretch()
  438. def closeEvent(self, event):
  439. """窗口关闭时停止状态监控线程"""
  440. if hasattr(self, 'state_monitor'):
  441. self.state_monitor.stop()
  442. super().closeEvent(event)