import sys import requests from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QLineEdit, QLabel, QComboBox, QPushButton, QSplitter, QTabWidget, QTextEdit, QMessageBox ) from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtCore import Qt, QUrl, pyqtSignal, QThread from PyQt5.QtGui import QColor, QBrush # --- 配置区 --- # 你的 API 基础地址 BASE_URL = "https://shanwogou.cn/audio/" API_URL = BASE_URL + "api.php" USER_API_URL = BASE_URL + "user_info_api.php" PLAY_URL_TEMPLATE = BASE_URL + "play.php?play={music_id}" # --- API 交互模块 --- def fetch_api_data(url, params=None): """通用函数,用于发送 GET 请求并解析 JSON 响应""" try: response = requests.get(url, params=params, timeout=15) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"API 请求失败: {e}") return None def get_all_music(): """获取所有音乐""" return fetch_api_data(API_URL, params={"action": "getAllMusic"}) def get_music_tags(): """获取所有音乐标签""" return fetch_api_data(API_URL, params={"action": "getMusicTags"}) def get_announcements(): """获取公告""" return fetch_api_data(API_URL, params={"action": "getAnnouncements"}) # --- 后台数据加载线程 --- class DataLoaderThread(QThread): """后台线程,用于加载数据,防止UI卡顿""" music_loaded_signal = pyqtSignal(dict) tags_loaded_signal = pyqtSignal(dict) announcements_loaded_signal = pyqtSignal(dict) def run(self): """线程执行函数""" # 顺序加载数据 music_data = get_all_music() self.music_loaded_signal.emit(music_data) tags_data = get_music_tags() self.tags_loaded_signal.emit(tags_data) announcements_data = get_announcements() self.announcements_loaded_signal.emit(announcements_data) # --- 主窗口类 --- class SunsetMusicApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("落日音乐 for PC") self.setGeometry(100, 100, 1200, 700) # 数据存储 self.all_music = [] self.filtered_music = [] # 初始化过滤条件属性(修复错误的关键) self.current_keyword = "" self.current_tag = "所有标签" self._init_ui() self._load_data_in_background() def _init_ui(self): """初始化用户界面""" central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 1. 顶部控制区 top_control_layout = QHBoxLayout() # 搜索框 self.search_input = QLineEdit() self.search_input.setPlaceholderText("输入关键词搜索...") self.search_input.textChanged.connect(self._filter_music_by_search) top_control_layout.addWidget(self.search_input) # 标签筛选下拉框 self.tag_combo = QComboBox() self.tag_combo.addItem("所有标签") self.tag_combo.currentIndexChanged.connect(self._filter_music_by_tag) top_control_layout.addWidget(self.tag_combo) # 刷新按钮 refresh_button = QPushButton("刷新列表") refresh_button.clicked.connect(self._load_data_in_background) top_control_layout.addWidget(refresh_button) main_layout.addLayout(top_control_layout) # 2. 中部主内容区(分割窗口) splitter = QSplitter(Qt.Horizontal) # 左侧音乐列表 self.music_tree = QTreeWidget() self.music_tree.setColumnCount(4) self.music_tree.setHeaderLabels(["ID", "标题", "艺术家", "标签"]) # 设置列宽 self.music_tree.setColumnWidth(0, 60) self.music_tree.setColumnWidth(1, 350) self.music_tree.setColumnWidth(2, 200) self.music_tree.setColumnWidth(3, 120) # 设置为单选 self.music_tree.setSelectionMode(QTreeWidget.SingleSelection) self.music_tree.itemClicked.connect(self._on_music_item_clicked) splitter.addWidget(self.music_tree) # 右侧内容区(标签页) self.right_tab_widget = QTabWidget() # 播放页签 self.play_web_view = QWebEngineView() # 初始加载一个占位页面 self.play_web_view.setUrl(QUrl("about:blank")) self.right_tab_widget.addTab(self.play_web_view, "播放器") # 公告页签 self.announcements_text = QTextEdit() self.announcements_text.setReadOnly(True) self.right_tab_widget.addTab(self.announcements_text, "公告") splitter.addWidget(self.right_tab_widget) # 设置拉伸比例,让右侧占更多空间 splitter.setSizes([600, 800]) main_layout.addWidget(splitter) def _load_data_in_background(self): """启动后台线程加载数据""" self._show_loading_indicator(True) self.loader_thread = DataLoaderThread() self.loader_thread.music_loaded_signal.connect(self._on_music_data_loaded) self.loader_thread.tags_loaded_signal.connect(self._on_tags_data_loaded) self.loader_thread.announcements_loaded_signal.connect(self._on_announcements_data_loaded) self.loader_thread.finished.connect(lambda: self._show_loading_indicator(False)) self.loader_thread.start() def _show_loading_indicator(self, is_loading): """显示或隐藏加载状态""" if is_loading: self.statusBar().showMessage("正在加载数据...") self.setEnabled(False) # 禁用UI else: self.statusBar().clearMessage() self.setEnabled(True) # 启用UI def _on_music_data_loaded(self, data): """处理加载完成的音乐数据""" if data and data.get("success", False): self.all_music = data.get("data", []) self.filtered_music = self.all_music.copy() self._populate_music_tree() print(f"成功加载 {len(self.all_music)} 首音乐。") else: QMessageBox.warning(self, "警告", "音乐数据加载失败!") print("音乐数据加载失败。") def _on_tags_data_loaded(self, data): """处理加载完成的标签数据""" if data and data.get("success", False): tags = data.get("data", []) self.tag_combo.clear() self.tag_combo.addItem("所有标签") self.tag_combo.addItems(tags) print(f"成功加载 {len(tags)} 个标签。") else: QMessageBox.warning(self, "警告", "标签数据加载失败!") print("标签数据加载失败。") def _on_announcements_data_loaded(self, data): """处理加载完成的公告数据""" self.announcements_text.clear() if data and data.get("success", False): announcements = data.get("data", []) for ann in announcements: # 假设字段是 'nr' (内容) 和 'time' (时间) content = ann.get("nr", "无内容") time_str = ann.get("time", "未知时间") self.announcements_text.append(f"【{time_str}】\n{content}\n") print(f"成功加载 {len(announcements)} 条公告。") else: self.announcements_text.setText("公告加载失败或暂无公告。") print("公告数据加载失败。") def _populate_music_tree(self): """将音乐数据填充到 TreeWidget 中""" self.music_tree.clear() for music in self.filtered_music: item = QTreeWidgetItem([ str(music.get("id", "")), music.get("title", "未知标题"), music.get("artist", "未知艺术家"), music.get("category", "未知标签") ]) # 存储完整音乐对象,方便后续使用 item.setData(0, Qt.UserRole, music) self.music_tree.addTopLevelItem(item) def _filter_music_by_search(self): """根据搜索框内容过滤音乐列表""" keyword = self.search_input.text().lower() self._apply_filters(keyword=keyword) def _filter_music_by_tag(self): """根据选择的标签过滤音乐列表""" selected_tag = self.tag_combo.currentText() self._apply_filters(tag=selected_tag) def _apply_filters(self, keyword=None, tag=None): """应用所有过滤器(搜索词和标签)""" # 使用类变量存储当前过滤条件 if keyword is not None: self.current_keyword = keyword if tag is not None: self.current_tag = tag filtered = [ music for music in self.all_music if (self.current_tag == "所有标签" or music.get("category", "") == self.current_tag) and (self.current_keyword == "" or self.current_keyword in music.get("title", "").lower() or self.current_keyword in music.get("artist", "").lower() or self.current_keyword in music.get("category", "").lower()) ] self.filtered_music = filtered self._populate_music_tree() self._highlight_search_results() def _highlight_search_results(self): """高亮显示搜索结果中的关键词""" keyword = self.current_keyword if not keyword: return # 注意:QTreeWidget本身不直接支持HTML格式,这里我们使用简单的颜色标记方式 for i in range(self.music_tree.topLevelItemCount()): item = self.music_tree.topLevelItem(i) for col in range(item.columnCount()): text = item.text(col) if keyword.lower() in text.lower(): # 找到匹配项,设置背景色 item.setBackground(col, QBrush(QColor(255, 255, 153))) # 浅黄色背景 else: # 没有匹配项,恢复默认背景 item.setBackground(col, QBrush(QColor(255, 255, 255))) def _on_music_item_clicked(self, item, column): """当音乐列表项被点击时,在播放器中加载对应的播放页面""" music = item.data(0, Qt.UserRole) if not music: return music_id = music.get("id") if music_id: play_url = PLAY_URL_TEMPLATE.format(music_id=music_id) self.play_web_view.setUrl(QUrl(play_url)) # 切换到播放器标签页 self.right_tab_widget.setCurrentIndex(0) print(f"正在播放: {music.get('title')} URL: {play_url}") # --- 程序入口 --- if __name__ == '__main__': app = QApplication(sys.argv) window = SunsetMusicApp() window.show() sys.exit(app.exec_())