| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- #!/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)
|