#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import os # 添加项目根目录到 Python 路径 current_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(current_dir) sys.path.append(project_root) import yaml import time import threading from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, QFileDialog, QGroupBox, QScrollArea, QSlider, QSpinBox, QComboBox ) from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal, QObject from PyQt6.QtGui import QFont from kodi_util.kodi_module import KodiClient from kodi_util.kodi_play.thread import KodiStateMonitorThread class ClientWidget(QWidget): """单个客户端控制窗口""" def __init__(self, client: KodiClient, client_idx: int, state_monitor: KodiStateMonitorThread, title: str, config_path: str, parent=None): super().__init__(parent) self.client = client self.client_idx = client_idx # 客户端在列表中的索引 self.state_monitor = state_monitor # 状态监控线程引用 self.title = title self.config_path = config_path # 保存配置文件路径 self.current_video_path = None # 当前播放的视频路径 # 创建定时器用于UI更新 self.ui_update_timer = QTimer(self) self.ui_update_timer.timeout.connect(self.update_ui_from_status) self.ui_update_timer.start(1000) # 每1秒更新一次UI # 创建音量调节去抖动定时器 self.volume_timer = QTimer(self) self.volume_timer.setSingleShot(True) self.volume_timer.timeout.connect(self.apply_volume_change) self.pending_volume = None # 加载视频列表 self.video_paths = self.load_video_paths() self.init_ui() def load_video_paths(self): """从配置文件加载视频路径列表""" try: with open(self.config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) return config.get('video_paths', []) except Exception as e: print(f"加载视频路径列表失败: {str(e)}") return [] def init_ui(self): """初始化客户端窗口UI""" layout = QVBoxLayout(self) # 创建分组框 group_box = QGroupBox(self.title) group_layout = QVBoxLayout() # 状态显示区域 status_frame = QFrame() status_frame.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken) status_layout = QVBoxLayout(status_frame) # 连接状态 self.connection_label = QLabel("连接状态: 检查中...") self.connection_label.setStyleSheet("QLabel { color: gray; }") status_layout.addWidget(self.connection_label) # 播放状态 self.playback_label = QLabel("播放状态: 未播放") self.playback_label.setStyleSheet("QLabel { color: gray; }") status_layout.addWidget(self.playback_label) # 地址显示 address_label = QLabel(f"地址: {self.client.url}") status_layout.addWidget(address_label) group_layout.addWidget(status_frame) # 音量控制区域 volume_frame = QFrame() volume_frame.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken) volume_layout = QHBoxLayout(volume_frame) # 音量标签 volume_label = QLabel("音量:") volume_layout.addWidget(volume_label) # 音量滑块 self.volume_slider = QSlider(Qt.Orientation.Horizontal) self.volume_slider.setMinimum(0) self.volume_slider.setMaximum(100) self.volume_slider.setValue(50) # 默认音量50% self.volume_slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.volume_slider.setTickInterval(10) self.volume_slider.valueChanged.connect(self.on_volume_changed) volume_layout.addWidget(self.volume_slider) # 音量数值显示 self.volume_spinbox = QSpinBox() self.volume_spinbox.setMinimum(0) self.volume_spinbox.setMaximum(100) self.volume_spinbox.setValue(50) self.volume_spinbox.valueChanged.connect(self.volume_slider.setValue) volume_layout.addWidget(self.volume_spinbox) # 音量百分比标签 self.volume_percent_label = QLabel("50%") volume_layout.addWidget(self.volume_percent_label) group_layout.addWidget(volume_frame) # 视频控制区域 video_control_frame = QFrame() video_control_frame.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Sunken) video_control_layout = QHBoxLayout(video_control_frame) # 视频选择下拉列表 self.video_combo = QComboBox() self.video_combo.addItems([os.path.basename(path) for path in self.video_paths]) self.video_combo.setMinimumWidth(200) video_control_layout.addWidget(self.video_combo) # 播放/暂停按钮 self.play_pause_btn = QPushButton("播放") self.play_pause_btn.setEnabled(False) self.play_pause_btn.clicked.connect(self.toggle_play_pause) video_control_layout.addWidget(self.play_pause_btn) # 停止按钮 self.stop_btn = QPushButton("停止") self.stop_btn.setEnabled(False) self.stop_btn.clicked.connect(self.stop_playback) video_control_layout.addWidget(self.stop_btn) # 当前播放文件名标签 self.current_file_label = QLabel("当前文件: 无") video_control_layout.addWidget(self.current_file_label) group_layout.addWidget(video_control_frame) # 按钮布局 button_layout = QHBoxLayout() # 播放视频按钮 self.video_btn = QPushButton("播放视频") self.video_btn.clicked.connect(self.play_video) button_layout.addWidget(self.video_btn) # 播放图片按钮 self.image_btn = QPushButton("播放图片") self.image_btn.clicked.connect(self.play_image) button_layout.addWidget(self.image_btn) group_layout.addLayout(button_layout) group_box.setLayout(group_layout) layout.addWidget(group_box) def toggle_play_pause(self): """切换播放/暂停状态""" try: # 获取当前播放器状态 state = self.state_monitor.get_state(self.client_idx) is_playing = state.get('playing', False) if not is_playing: # 如果没有在播放,尝试播放当前选中的视频 selected_index = self.video_combo.currentIndex() if selected_index >= 0 and selected_index < len(self.video_paths): video_path = self.video_paths[selected_index] self.client.play_video(video_path) self.current_video_path = video_path self.current_file_label.setText(f"当前文件: {os.path.basename(video_path)}") else: self.playback_label.setText("播放状态: 请先选择要播放的视频") self.playback_label.setStyleSheet("QLabel { color: red; }") return else: # 如果正在播放,尝试暂停/继续 self.client.pause_playback() except Exception as e: print(f"切换播放/暂停状态失败: {str(e)}") self.playback_label.setText(f"播放状态: 操作失败 - {str(e)}") self.playback_label.setStyleSheet("QLabel { color: red; }") def stop_playback(self): """停止播放""" try: self.client.stop_playback() self.current_video_path = None self.current_file_label.setText("当前文件: 无") self.play_pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.play_pause_btn.setText("播放") except Exception as e: print(f"停止播放失败: {str(e)}") def on_volume_changed(self, value): """音量滑块值改变时的处理函数""" self.volume_spinbox.setValue(value) self.volume_percent_label.setText(f"{value}%") # 存储待更新的音量值 self.pending_volume = value # 重置定时器,300毫秒后应用音量变化 self.volume_timer.start(300) def apply_volume_change(self): """实际应用音量变化的函数""" if self.pending_volume is not None: try: self.client.set_volume(self.pending_volume) self.pending_volume = None except Exception as e: print(f"设置音量失败: {str(e)}") def update_ui_from_status(self): """从状态监控线程获取状态数据并更新UI""" # 获取客户端状态 state = self.state_monitor.get_state(self.client_idx) # 更新连接状态UI is_connected = state.get('connected', False) if is_connected: self.connection_label.setText("连接状态: 已连接") self.connection_label.setStyleSheet("QLabel { color: green; }") self.video_btn.setEnabled(True) self.image_btn.setEnabled(True) self.volume_slider.setEnabled(True) self.volume_spinbox.setEnabled(True) self.play_pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) # 更新音量显示 volume = state.get('volume', 50) self.volume_slider.setValue(volume) self.volume_spinbox.setValue(volume) self.volume_percent_label.setText(f"{volume}%") else: self.connection_label.setText("连接状态: 未连接") self.connection_label.setStyleSheet("QLabel { color: red; }") self.video_btn.setEnabled(False) self.image_btn.setEnabled(False) self.volume_slider.setEnabled(False) self.volume_spinbox.setEnabled(False) self.play_pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.playback_label.setText("播放状态: 未连接") self.playback_label.setStyleSheet("QLabel { color: gray; }") return # 更新播放状态UI is_playing = state.get('playing', False) is_paused = state.get('paused', False) if is_playing: current_time = self._format_time(state.get('time', 0)) total_time = self._format_time(state.get('totaltime', 0)) status_text = "暂停" if is_paused else "正在播放" self.playback_label.setText(f"播放状态: {status_text} ({current_time}/{total_time})") self.playback_label.setStyleSheet("QLabel { color: blue; }") self.play_pause_btn.setText("暂停" if not is_paused else "播放") # 更新当前文件显示 current_file = state.get('current_file', '') if current_file: self.current_file_label.setText(f"当前文件: {os.path.basename(current_file)}") else: self.play_pause_btn.setText("播放") self.playback_label.setText("播放状态: 未播放") self.playback_label.setStyleSheet("QLabel { color: gray; }") def _format_time(self, seconds): """将秒数格式化为时:分:秒格式""" hours = seconds // 3600 minutes = (seconds % 3600) // 60 seconds = seconds % 60 if hours > 0: return f"{hours:02d}:{minutes:02d}:{seconds:02d}" return f"{minutes:02d}:{seconds:02d}" def play_video(self): """播放视频按钮点击处理""" selected_index = self.video_combo.currentIndex() if selected_index >= 0 and selected_index < len(self.video_paths): video_path = self.video_paths[selected_index] try: self.client.play_video(video_path) self.current_video_path = video_path self.current_file_label.setText(f"当前文件: {os.path.basename(video_path)}") self.play_pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) except Exception as e: self.playback_label.setText(f"播放状态: 播放失败 - {str(e)}") self.playback_label.setStyleSheet("QLabel { color: red; }") def play_image(self): """播放图片按钮点击处理""" file_path, _ = QFileDialog.getOpenFileName( self, "选择图片文件", "", "图片文件 (*.jpg *.jpeg *.png *.bmp);;所有文件 (*.*)" ) if file_path: try: self.client.play_image(file_path) self.current_file_label.setText(f"当前文件: {os.path.basename(file_path)}") except Exception as e: self.playback_label.setText(f"播放状态: 显示失败 - {str(e)}") self.playback_label.setStyleSheet("QLabel { color: red; }") def closeEvent(self, event): """窗口关闭时停止定时器""" self.ui_update_timer.stop() super().closeEvent(event) def update_video_list(self): """更新视频列表""" # 重新加载视频路径 self.video_paths = self.load_video_paths() # 保存当前选中的视频 current_text = self.video_combo.currentText() # 清空并重新添加视频列表 self.video_combo.clear() self.video_combo.addItems([os.path.basename(path) for path in self.video_paths]) # 尝试恢复之前选中的视频 index = self.video_combo.findText(current_text) if index >= 0: self.video_combo.setCurrentIndex(index) class DebugWindow(QMainWindow): """播放控制主窗口""" def __init__(self, config_path=None): super().__init__() # 设置配置文件路径 self.config_path = config_path or os.path.join("config", "config.yaml") # 设置窗口标题和大小 self.setWindowTitle("调试模块") self.setMinimumSize(QSize(800, 600)) # 创建中央部件 self.central_widget = QWidget() self.setCentralWidget(self.central_widget) # 创建主布局 self.main_layout = QVBoxLayout(self.central_widget) self.main_layout.setContentsMargins(10, 10, 10, 10) self.main_layout.setSpacing(10) # 创建配置文件选择区域 config_layout = QHBoxLayout() # 配置文件路径显示 self.config_label = QLabel(f"当前配置文件: {self.config_path}") self.config_label.setFont(QFont("Arial", 10)) config_layout.addWidget(self.config_label) # 选择配置文件按钮 select_config_btn = QPushButton("选择配置文件") select_config_btn.clicked.connect(self.select_config_file) config_layout.addWidget(select_config_btn) # 重新加载按钮 reload_btn = QPushButton("重新加载") reload_btn.clicked.connect(self.reload_config) config_layout.addWidget(reload_btn) self.main_layout.addLayout(config_layout) # 创建滚动区域 self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_widget = QWidget() self.scroll_layout = QVBoxLayout(self.scroll_widget) self.scroll_area.setWidget(self.scroll_widget) self.main_layout.addWidget(self.scroll_area) # 客户端列表 self.clients = [] # 初始化UI self.init_ui() def select_config_file(self): """选择配置文件""" file_path, _ = QFileDialog.getOpenFileName( self, "选择配置文件", "", "YAML文件 (*.yaml *.yml);;所有文件 (*.*)" ) if file_path: self.config_path = file_path self.config_label.setText(f"当前配置文件: {self.config_path}") self.reload_config() def reload_config(self): """重新加载配置""" # 清除现有的客户端窗口 while self.scroll_layout.count(): item = self.scroll_layout.takeAt(0) if item.widget(): item.widget().deleteLater() # 清空客户端列表 self.clients = [] # 停止状态监控线程 if hasattr(self, 'state_monitor'): self.state_monitor.stop() # 重新初始化UI self.init_ui() # 更新所有客户端的视频列表 for i in range(self.scroll_layout.count()): widget = self.scroll_layout.itemAt(i).widget() if isinstance(widget, ClientWidget): widget.update_video_list() def load_config(self): """从配置文件加载客户端配置 Returns: list: 客户端配置列表,如果加载失败则返回默认配置 """ # 默认配置 default_config = { 'kodi_clients': [ { 'ip': 'localhost', 'port': 8080, 'username': 'kodi', 'password': '123' } ] } try: # 检查配置文件是否存在 if not os.path.exists(self.config_path): print(f"警告:配置文件 {self.config_path} 不存在,使用默认配置") return default_config['kodi_clients'] # 读取配置文件 with open(self.config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) # 验证配置文件格式 if not config or 'kodi_clients' not in config: print(f"警告:配置文件 {self.config_path} 格式错误,使用默认配置") return default_config['kodi_clients'] return config['kodi_clients'] except Exception as e: print(f"加载配置文件失败: {str(e)},使用默认配置") return default_config['kodi_clients'] def init_ui(self): """初始化用户界面""" # 从配置文件加载客户端信息 clients_config = self.load_config() if not clients_config: # 如果没有找到配置,显示错误信息 error_label = QLabel("错误:未能加载客户端配置") error_label.setStyleSheet("QLabel { color: red; }") self.scroll_layout.addWidget(error_label) return # 创建客户端列表 for client_config in clients_config: try: # 创建客户端实例,确保使用正确的参数名 client = KodiClient( host=client_config.get('ip', 'localhost'), # 使用ip字段作为host参数 port=client_config.get('port', 8080), username=client_config.get('username'), password=client_config.get('password') ) print(f"创建客户端: host={client.host}, port={client.port}") # 添加调试信息 self.clients.append(client) except Exception as e: print(f"创建客户端失败: {str(e)}") # 创建并启动状态监控线程 self.state_monitor = KodiStateMonitorThread(self.clients) self.state_monitor.start() # 为每个客户端创建控制窗口 for i, client in enumerate(self.clients): try: # 创建客户端窗口,传入配置文件路径 client_widget = ClientWidget(client, i, self.state_monitor, f"客户端 {i+1}", self.config_path) self.scroll_layout.addWidget(client_widget) except Exception as e: # 如果创建客户端窗口失败,显示错误信息 error_label = QLabel(f"客户端 {i+1} 窗口创建失败: {str(e)}") error_label.setStyleSheet("QLabel { color: red; }") self.scroll_layout.addWidget(error_label) # 添加一个弹性空间 self.scroll_layout.addStretch() def closeEvent(self, event): """窗口关闭时停止状态监控线程""" if hasattr(self, 'state_monitor'): self.state_monitor.stop() super().closeEvent(event)