diff --git a/Beta-0.1.1.py b/Beta-0.1.1.py new file mode 100644 index 0000000..4d71959 --- /dev/null +++ b/Beta-0.1.1.py @@ -0,0 +1,283 @@ +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_()) diff --git a/Beta-0.1.2.py b/Beta-0.1.2.py new file mode 100644 index 0000000..15b01c6 --- /dev/null +++ b/Beta-0.1.2.py @@ -0,0 +1,725 @@ +import sys +import requests +import pygame +import time +import os +from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QTreeWidget, QTreeWidgetItem, QLineEdit, QLabel, QComboBox, + QPushButton, QSplitter, QTabWidget, QTextEdit, QMessageBox, + QFrame, QAction, QMenu, QSlider +) +from PyQt5.QtCore import Qt, pyqtSignal, QThread, QTimer, QObject +from PyQt5.QtGui import QColor, QBrush + +# --- 初始化 pygame 音频模块 --- +pygame.mixer.init() + +# --- 配置区 --- +BASE_URL = "https://shanwogou.cn/audio/" +API_URL = BASE_URL + "api.php" + +# --- API 交互模块 --- +def fetch_api_data(url, params=None): + """通用 GET 请求函数,解析 JSON 响应""" + try: + response = requests.get(url, params=params, timeout=15) + response.raise_for_status() # 触发 HTTP 错误(如 404/500) + return response.json() + except requests.exceptions.RequestException as e: + print(f"API 请求失败: {str(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"}) + +def get_music_url(music_id): + """获取音乐的直接播放地址(mp3 链接)""" + return fetch_api_data(API_URL, params={"action": "getMusicUrl", "id": music_id}) + +# --- 后台音频下载线程 --- +class AudioDownloadThread(QThread): + """后台下载音频文件,下载完成后发送信号""" + download_finished = pyqtSignal(str, str) # 信号:音乐标题、本地临时文件路径 + download_failed = pyqtSignal(str) # 信号:失败原因 + download_progress = pyqtSignal(int) # 信号:下载进度(0-100) + + def __init__(self, music_id, music_title): + super().__init__() + self.music_id = music_id + self.music_title = music_title + self.is_canceled = False # 取消下载标记 + + def run(self): + try: + # 1. 获取音乐的直接播放链接 + music_url_data = get_music_url(self.music_id) + if not music_url_data or not music_url_data.get("success", False): + self.download_failed.emit("未获取到有效的播放链接") + return + + play_url = music_url_data["data"].get("play_url") + if not play_url: + self.download_failed.emit("API 未返回直接播放地址(play_url)") + return + + # 2. 下载音频文件到临时目录 + temp_file = f"temp_{self.music_id}_{int(time.time())}.mp3" + response = requests.get(play_url, stream=True, timeout=20) + response.raise_for_status() + + # 获取文件总大小(用于计算进度) + total_size = int(response.headers.get("content-length", 0)) + downloaded_size = 0 + + with open(temp_file, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + if self.is_canceled: # 检查是否取消下载 + if os.path.exists(temp_file): + os.remove(temp_file) + self.download_failed.emit("下载已取消") + return + if chunk: + f.write(chunk) + downloaded_size += len(chunk) + # 计算并发送进度(避免除零错误) + if total_size > 0: + progress = int((downloaded_size / total_size) * 100) + self.download_progress.emit(progress) + + # 3. 下载完成,发送信号 + self.download_finished.emit(self.music_title, temp_file) + + except Exception as e: + self.download_failed.emit(f"下载失败:{str(e)}") + + def cancel_download(self): + """取消当前下载任务""" + self.is_canceled = True + +# --- 数据加载工作类(继承自QObject,支持moveToThread)--- +class DataLoadWorker(QObject): + """数据加载工作类(与主线程分离,避免UI卡顿)""" + music_loaded = pyqtSignal(dict) + tags_loaded = pyqtSignal(dict) + announcements_loaded = pyqtSignal(dict) + finished = pyqtSignal() + + def run(self): + """加载音乐、标签、公告数据""" + # 1. 加载音乐 + music_data = get_all_music() + self.music_loaded.emit(music_data) + + # 2. 加载标签 + tags_data = get_music_tags() + self.tags_loaded.emit(tags_data) + + # 3. 加载公告 + announcements_data = get_announcements() + self.announcements_loaded.emit(announcements_data) + + # 4. 发送完成信号 + self.finished.emit() + +# --- 主窗口类 --- +class SunsetMusicApp(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Sunset Music for PC") + self.setGeometry(100, 100, 1200, 700) + self.setMinimumSize(1000, 600) # 最小窗口尺寸 + + # 数据存储 + self.all_music = [] # 所有音乐列表 + self.filtered_music = [] # 筛选后音乐列表 + self.current_playing_file = ""# 当前播放的本地音频文件路径 + self.current_playing_music = None # 当前播放的音乐信息(字典) + self.download_thread = None # 音频下载线程实例 + self.is_playing = False # 播放状态标记 + + # 过滤条件初始化 + self.current_keyword = "" + self.current_tag = "所有标签" + + # 初始化 UI + self.init_ui() + # 加载音乐、标签、公告数据 + self.load_data_in_background() + # 初始化播放进度定时器 + self.init_playback_timer() + + def init_ui(self): + """构建完整 UI 界面""" + central_widget = QWidget() + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout(central_widget) + + # 1. 菜单栏 + self.create_menu_bar() + + # 2. 顶部控制区(搜索+筛选+刷新) + top_control_layout = QHBoxLayout() + top_control_layout.setContentsMargins(10, 10, 10, 5) + + # 搜索框 + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("搜索:标题/艺术家/标签...") + self.search_input.setFixedHeight(30) + self.search_input.textChanged.connect(self.filter_music_by_search) + top_control_layout.addWidget(self.search_input, stretch=3) + + # 标签筛选下拉框 + self.tag_combo = QComboBox() + self.tag_combo.addItem("所有标签") + self.tag_combo.setFixedHeight(30) + self.tag_combo.setFixedWidth(150) + self.tag_combo.currentIndexChanged.connect(self.filter_music_by_tag) + top_control_layout.addWidget(self.tag_combo, stretch=1) + + # 刷新按钮 + self.refresh_btn = QPushButton("刷新列表") + self.refresh_btn.setFixedHeight(30) + self.refresh_btn.setFixedWidth(100) + self.refresh_btn.clicked.connect(self.load_data_in_background) + top_control_layout.addWidget(self.refresh_btn, stretch=0) + + main_layout.addLayout(top_control_layout) + + # 3. 中部主内容区(分割窗口:音乐列表 + 右侧内容) + splitter = QSplitter(Qt.Horizontal) + splitter.setContentsMargins(10, 5, 10, 10) + + # 3.1 左侧:音乐列表 + self.music_tree = QTreeWidget() + self.music_tree.setColumnCount(5) + self.music_tree.setHeaderLabels(["ID", "标题", "艺术家", "标签", "时长"]) + # 设置列宽 + self.music_tree.setColumnWidth(0, 60) # ID 列 + self.music_tree.setColumnWidth(1, 350) # 标题列 + self.music_tree.setColumnWidth(2, 200) # 艺术家列 + self.music_tree.setColumnWidth(3, 120) # 标签列 + self.music_tree.setColumnWidth(4, 80) # 时长列 + # 单选模式 + 双击播放 + self.music_tree.setSelectionMode(QTreeWidget.SingleSelection) + self.music_tree.itemDoubleClicked.connect(self.on_music_double_click) + splitter.addWidget(self.music_tree) + + # 3.2 右侧:标签页(播放器 + 公告) + self.right_tab = QTabWidget() + self.right_tab.setMinimumWidth(500) + + # 右侧1:播放器面板 + self.player_panel = QWidget() + self.init_player_panel() # 初始化播放控制UI + self.right_tab.addTab(self.player_panel, "音乐播放器") + + # 右侧2:公告面板 + self.announcement_panel = QTextEdit() + self.announcement_panel.setReadOnly(True) + self.announcement_panel.setStyleSheet("font-size: 14px; padding: 10px;") + self.right_tab.addTab(self.announcement_panel, "最新公告") + + splitter.addWidget(self.right_tab) + splitter.setSizes([600, 600]) # 左右窗口比例 + main_layout.addWidget(splitter, stretch=1) + + # 4. 底部状态栏 + self.status_bar = self.statusBar() + self.status_bar.showMessage("就绪:未播放音乐") + + def create_menu_bar(self): + """创建菜单栏(文件+帮助)""" + menubar = self.menuBar() + + # 文件菜单 + file_menu = menubar.addMenu("文件") + exit_action = QAction("退出", self) + exit_action.setShortcut("Ctrl+Q") + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # 帮助菜单 + help_menu = menubar.addMenu("帮助") + about_action = QAction("关于", self) + about_action.triggered.connect(self.show_about_dialog) + help_menu.addAction(about_action) + + def init_player_panel(self): + """初始化播放器面板""" + panel_layout = QVBoxLayout(self.player_panel) + panel_layout.setContentsMargins(20, 20, 20, 20) + + # 1. 当前播放音乐信息 + self.music_info_label = QLabel("当前未播放音乐") + self.music_info_label.setAlignment(Qt.AlignCenter) + self.music_info_label.setStyleSheet(""" + font-size: 16px; + font-weight: bold; + color: #2c3e50; + margin-bottom: 20px; + """) + panel_layout.addWidget(self.music_info_label) + + # 2. 下载进度条(默认隐藏) + self.download_progress = QSlider(Qt.Horizontal) + self.download_progress.setRange(0, 100) + self.download_progress.setValue(0) + self.download_progress.setDisabled(True) + self.download_progress.setHidden(True) + panel_layout.addWidget(self.download_progress) + + # 3. 播放进度条 + self.play_progress = QSlider(Qt.Horizontal) + self.play_progress.setRange(0, 100) + self.play_progress.setValue(0) + self.play_progress.setDisabled(True) + self.play_progress.sliderReleased.connect(self.seek_playback) + panel_layout.addWidget(self.play_progress) + + # 4. 时间显示(已播放/总时长) + time_layout = QHBoxLayout() + self.current_time_label = QLabel("00:00") + self.total_time_label = QLabel("00:00") + time_layout.addWidget(self.current_time_label) + time_layout.addStretch(1) + time_layout.addWidget(self.total_time_label) + panel_layout.addLayout(time_layout) + + # 5. 播放控制按钮 + control_layout = QHBoxLayout() + control_layout.setAlignment(Qt.AlignCenter) + + self.prev_btn = QPushButton("上一曲") + self.prev_btn.setFixedSize(80, 40) + self.prev_btn.setDisabled(True) + self.prev_btn.clicked.connect(self.play_previous) + control_layout.addWidget(self.prev_btn) + + self.play_pause_btn = QPushButton("播放") + self.play_pause_btn.setFixedSize(100, 40) + self.play_pause_btn.setDisabled(True) + self.play_pause_btn.clicked.connect(self.toggle_play_pause) + control_layout.addWidget(self.play_pause_btn) + + self.next_btn = QPushButton("下一曲") + self.next_btn.setFixedSize(80, 40) + self.next_btn.setDisabled(True) + self.next_btn.clicked.connect(self.play_next) + control_layout.addWidget(self.next_btn) + + panel_layout.addLayout(control_layout) + + def init_playback_timer(self): + """初始化播放进度定时器""" + self.playback_timer = QTimer(self) + self.playback_timer.setInterval(1000) # 1秒=1000毫秒 + self.playback_timer.timeout.connect(self.update_playback_progress) + + def show_about_dialog(self): + """显示关于对话框""" + QMessageBox.about( + self, "关于落日音乐", + "落日音乐 for PC(应用内播放版)\n\n" + "版本:1.0.0\n" + "核心功能:应用内直接播放、音乐筛选、公告查看" + ) + + # --- 数据加载相关函数 --- + def load_data_in_background(self): + """后台加载音乐、标签、公告数据""" + self.status_bar.showMessage("正在加载数据...") + self.set_widgets_enabled(False) # 禁用UI控件 + + # 启动数据加载线程 + self.data_thread = QThread() + self.data_worker = DataLoadWorker() + self.data_worker.moveToThread(self.data_thread) + + # 连接线程信号 + self.data_thread.started.connect(self.data_worker.run) + self.data_worker.music_loaded.connect(self.on_music_data_loaded) + self.data_worker.tags_loaded.connect(self.on_tags_data_loaded) + self.data_worker.announcements_loaded.connect(self.on_announcements_loaded) + self.data_worker.finished.connect(self.data_thread.quit) + self.data_worker.finished.connect(lambda: self.set_widgets_enabled(True)) + self.data_worker.finished.connect(lambda: self.status_bar.showMessage("数据加载完成")) + + self.data_thread.start() + + def set_widgets_enabled(self, enabled): + """设置核心UI控件是否可用""" + self.search_input.setEnabled(enabled) + self.tag_combo.setEnabled(enabled) + self.refresh_btn.setEnabled(enabled) + self.music_tree.setEnabled(enabled) + + def on_music_data_loaded(self, music_data): + """音乐数据加载完成后的处理""" + if not music_data or not music_data.get("success", False): + QMessageBox.warning(self, "警告", "音乐数据加载失败!") + return + + self.all_music = music_data["data"] + self.filtered_music = self.all_music.copy() + self.populate_music_tree() # 填充音乐列表 + self.status_bar.showMessage(f"加载完成:共 {len(self.all_music)} 首音乐") + + def on_tags_data_loaded(self, tags_data): + """标签数据加载完成后的处理""" + if not tags_data or not tags_data.get("success", False): + QMessageBox.warning(self, "警告", "标签数据加载失败!") + return + + tags = tags_data["data"] + current_tag = self.tag_combo.currentText() + self.tag_combo.clear() + self.tag_combo.addItem("所有标签") + self.tag_combo.addItems(tags) + + # 恢复之前选择的标签 + if current_tag in tags: + self.tag_combo.setCurrentText(current_tag) + + def on_announcements_loaded(self, announcements_data): + """公告数据加载完成后的处理""" + self.announcement_panel.clear() + if not announcements_data or not announcements_data.get("success", False): + self.announcement_panel.setText("公告加载失败或暂无公告") + return + + announcements = announcements_data["data"] + for ann in announcements: + time_str = ann.get("time", "未知时间") + content = ann.get("nr", "无公告内容") + self.announcement_panel.append(f"【{time_str}】\n{content}\n" + "-"*50 + "\n") + + def populate_music_tree(self): + """将音乐数据填充到 TreeWidget 列表中""" + self.music_tree.clear() + for idx, music in enumerate(self.filtered_music): + # 格式化时长 + duration = music.get("duration", "") + try: + duration_sec = int(duration) + duration_str = f"{duration_sec//60:02d}:{duration_sec%60:02d}" + except: + duration_str = "未知" + + # 创建列表项 + item = QTreeWidgetItem([ + str(music.get("id", "未知")), + music.get("title", "未知标题"), + music.get("artist", "未知艺术家"), + music.get("category", "未知标签"), + duration_str + ]) + # 存储完整音乐信息 + item.setData(0, Qt.UserRole, music) + # 交替行颜色 + if idx % 2 == 0: + item.setBackground(0, QBrush(QColor(245, 245, 245))) + item.setBackground(1, QBrush(QColor(245, 245, 245))) + item.setBackground(2, QBrush(QColor(245, 245, 245))) + item.setBackground(3, QBrush(QColor(245, 245, 245))) + item.setBackground(4, QBrush(QColor(245, 245, 245))) + + self.music_tree.addTopLevelItem(item) + + # --- 音乐筛选相关函数 --- + def filter_music_by_search(self): + """根据搜索关键词筛选音乐""" + self.current_keyword = self.search_input.text().lower() + self.apply_music_filters() + + def filter_music_by_tag(self): + """根据标签筛选音乐""" + self.current_tag = self.tag_combo.currentText() + self.apply_music_filters() + + def apply_music_filters(self): + """应用筛选条件(关键词+标签)""" + self.filtered_music = [ + music for music in self.all_music + if (self.current_tag == "所有标签" or music.get("category", "") == self.current_tag) + and ( + 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.populate_music_tree() + self.highlight_search_keyword() + + def highlight_search_keyword(self): + """高亮音乐列表中的搜索关键词""" + if not self.current_keyword: + return + + # 遍历所有列表项,匹配关键词并高亮 + for idx in range(self.music_tree.topLevelItemCount()): + item = self.music_tree.topLevelItem(idx) + for col in range(1, 4): # 只高亮 标题、艺术家、标签 列 + text = item.text(col).lower() + if self.current_keyword in text: + item.setBackground(col, QBrush(QColor(255, 255, 153))) # 浅黄色高亮 + else: + # 恢复交替行颜色 + if idx % 2 == 0: + item.setBackground(col, QBrush(QColor(245, 245, 245))) + else: + item.setBackground(col, QBrush(QColor(255, 255, 255))) + + # --- 音乐播放相关函数 --- + def on_music_double_click(self, item, column): + """双击音乐列表项:停止当前播放→下载新音乐→播放""" + # 获取当前选中的音乐信息 + music = item.data(0, Qt.UserRole) + if not music: + return + + # 1. 停止当前播放(如果有) + self.stop_playback() + + # 2. 取消正在进行的下载(如果有) + if self.download_thread and self.download_thread.isRunning(): + self.download_thread.cancel_download() + self.download_thread.wait() + + # 3. 记录当前播放的音乐信息 + self.current_playing_music = music + music_id = music.get("id") + music_title = music.get("title", "未知音乐") + music_artist = music.get("artist", "未知艺术家") + + # 4. 更新UI显示 + self.music_info_label.setText(f"{music_title} - {music_artist}") + self.status_bar.showMessage(f"准备播放:{music_title}") + + # 5. 启动后台下载线程 + self.download_thread = AudioDownloadThread(music_id, music_title) + # 连接下载信号 + self.download_thread.download_progress.connect(self.update_download_progress) + self.download_thread.download_finished.connect(self.on_audio_downloaded) + self.download_thread.download_failed.connect(self.on_download_failed) + # 显示下载进度条 + self.download_progress.setHidden(False) + self.download_progress.setDisabled(False) + # 启动下载 + self.download_thread.start() + + def update_download_progress(self, progress): + """更新下载进度条""" + self.download_progress.setValue(progress) + self.status_bar.showMessage(f"正在下载:{self.current_playing_music['title']}({progress}%)") + + def on_audio_downloaded(self, music_title, local_file): + """音频下载完成,开始播放""" + # 隐藏下载进度条 + self.download_progress.setHidden(True) + self.download_progress.setDisabled(True) + self.download_progress.setValue(0) + + # 记录当前播放的本地文件路径 + self.current_playing_file = local_file + + # 尝试播放音频 + try: + pygame.mixer.music.load(local_file) + pygame.mixer.music.play() + self.is_playing = True + + # 更新UI状态 + self.play_pause_btn.setText("暂停") + self.play_pause_btn.setEnabled(True) + self.prev_btn.setEnabled(True) + self.next_btn.setEnabled(True) + self.play_progress.setDisabled(False) + + # 记录总时长并更新时间显示 + total_duration = pygame.mixer.Sound(local_file).get_length() # 秒 + self.total_duration = total_duration + self.total_time_label.setText(self.format_time(total_duration)) + + # 启动播放进度定时器 + self.playback_timer.start() + self.status_bar.showMessage(f"正在播放:{music_title}") + + except Exception as e: + self.on_download_failed(f"播放失败:{str(e)}") + + def on_download_failed(self, error_msg): + """下载或播放失败的处理""" + # 重置UI + self.download_progress.setHidden(True) + self.download_progress.setDisabled(True) + self.download_progress.setValue(0) + self.music_info_label.setText("播放失败") + self.status_bar.showMessage(f"错误:{error_msg}") + + # 弹出错误提示 + QMessageBox.warning(self, "播放错误", error_msg) + + def toggle_play_pause(self): + """切换播放/暂停状态""" + if not self.current_playing_file: + return + + if self.is_playing: + pygame.mixer.music.pause() + self.is_playing = False + self.play_pause_btn.setText("播放") + self.playback_timer.stop() + self.status_bar.showMessage(f"已暂停:{self.current_playing_music['title']}") + else: + pygame.mixer.music.unpause() + self.is_playing = True + self.play_pause_btn.setText("暂停") + self.playback_timer.start() + self.status_bar.showMessage(f"继续播放:{self.current_playing_music['title']}") + + def stop_playback(self): + """停止当前播放""" + if pygame.mixer.music.get_busy() or self.is_playing: + pygame.mixer.music.stop() + self.is_playing = False + + # 重置UI + self.playback_timer.stop() + self.play_progress.setValue(0) + self.current_time_label.setText("00:00") + self.total_time_label.setText("00:00") + self.play_pause_btn.setText("播放") + self.play_pause_btn.setEnabled(False) + self.prev_btn.setEnabled(False) + self.next_btn.setEnabled(False) + + # 删除临时音频文件 + if self.current_playing_file and os.path.exists(self.current_playing_file): + try: + os.remove(self.current_playing_file) + except: + pass # 若文件被占用,忽略删除 + self.current_playing_file = "" + self.current_playing_music = None + + def update_playback_progress(self): + """更新播放进度条和时间显示""" + if not self.current_playing_file or not self.is_playing: + return + + # 获取当前播放位置(秒) + current_pos = pygame.mixer.music.get_pos() / 1000 # 毫秒转秒 + # 计算进度百分比 + progress = int((current_pos / self.total_duration) * 100) + progress = min(progress, 100) # 避免超过100% + + # 更新UI + self.play_progress.setValue(progress) + self.current_time_label.setText(self.format_time(current_pos)) + + # 检查是否播放完成 + if current_pos >= self.total_duration: + self.stop_playback() + self.status_bar.showMessage("播放完成:已停止") + + def seek_playback(self): + """拖动进度条跳转播放位置""" + if not self.current_playing_file or not self.is_playing: + return + + # 获取进度条百分比对应的播放位置(秒) + progress = self.play_progress.value() + target_pos = (progress / 100) * self.total_duration + # 跳转播放位置 + pygame.mixer.music.set_pos(target_pos) + # 更新时间显示 + self.current_time_label.setText(self.format_time(target_pos)) + + def play_previous(self): + """播放上一曲""" + if not self.current_playing_music or len(self.filtered_music) <= 1: + return + + # 找到当前播放音乐在筛选列表中的索引 + current_idx = -1 + current_id = self.current_playing_music.get("id") + for idx, music in enumerate(self.filtered_music): + if str(music.get("id")) == str(current_id): + current_idx = idx + break + + # 播放前一项(循环到最后一项如果当前是第一项) + if current_idx == 0: + target_idx = len(self.filtered_music) - 1 + else: + target_idx = current_idx - 1 + + # 触发双击播放 + target_item = self.music_tree.topLevelItem(target_idx) + self.on_music_double_click(target_item, 0) + + def play_next(self): + """播放下一曲""" + if not self.current_playing_music or len(self.filtered_music) <= 1: + return + + # 找到当前播放音乐在筛选列表中的索引 + current_idx = -1 + current_id = self.current_playing_music.get("id") + for idx, music in enumerate(self.filtered_music): + if str(music.get("id")) == str(current_id): + current_idx = idx + break + + # 播放后一项(循环到第一项如果当前是最后一项) + if current_idx == len(self.filtered_music) - 1: + target_idx = 0 + else: + target_idx = current_idx + 1 + + # 触发双击播放 + target_item = self.music_tree.topLevelItem(target_idx) + self.on_music_double_click(target_item, 0) + + @staticmethod + def format_time(seconds): + """将秒数格式化为 分:秒""" + minutes = int(seconds // 60) + secs = int(seconds % 60) + return f"{minutes:02d}:{secs:02d}" + + def closeEvent(self, event): + """窗口关闭时的清理操作""" + # 停止播放 + self.stop_playback() + # 退出 pygame + pygame.mixer.quit() + # 关闭线程 + if hasattr(self, "download_thread") and self.download_thread.isRunning(): + self.download_thread.cancel_download() + self.download_thread.wait() + event.accept() + +# --- 程序入口 --- +if __name__ == "__main__": + # 解决 PyQt5 中文显示问题 + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + app = QApplication(sys.argv) + + # 设置全局样式 + app.setStyle("Fusion") + + # 启动应用 + window = SunsetMusicApp() + window.show() + sys.exit(app.exec_())