上传文件至 /

This commit is contained in:
2025-09-25 14:12:44 +00:00
parent 1438780f91
commit 566bd8b4f3

872
Beta-0.1.4.py Normal file
View File

@@ -0,0 +1,872 @@
import sys
import requests
import pygame
import os
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTreeWidget, QTreeWidgetItem, QLineEdit, QLabel, QPushButton,
QSplitter, QTabWidget, QTextEdit, QMessageBox, QAction,
QSlider, QButtonGroup, QProgressDialog
)
from PyQt5.QtCore import Qt, pyqtSignal, QThread, QTimer, QObject
from PyQt5.QtGui import QColor, QBrush, QFont, QTextDocument
# --- 初始化 pygame 音频模块 ---
pygame.mixer.init()
# --- 配置区 ---
BASE_URL = "https://shanwogou.cn/audio/"
API_URL = BASE_URL + "api.php"
# 本地音频存储路径(自动创建./audio目录
AUDIO_SAVE_DIR = "./audio"
os.makedirs(AUDIO_SAVE_DIR, exist_ok=True)
# 音乐分类配置(重命名+颜色)
CATEGORY_CONFIG = {
'all': {'name': '全部音乐', 'color': '#b89e81', 'text_color': '#5d4037'},
'cantonese': {'name': '粤语歌曲', 'color': '#c8e6c9', 'text_color': '#2e7d32'},
'mandarin': {'name': '国语歌曲', 'color': '#fff3e0', 'text_color': '#e65100'},
'waiyu': {'name': '外语歌曲', 'color': '#e3f2fd', 'text_color': '#0d47a1'},
'classic': {'name': '经典老歌', 'color': '#efebe9', 'text_color': '#3e2723'},
'other': {'name': '其他音乐', 'color': '#f3e5f5', 'text_color': '#6a1b9a'}
}
# --- 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 DownloadThread(QThread):
"""完整下载音乐文件的线程"""
progress_updated = pyqtSignal(int) # 下载进度
download_complete = pyqtSignal(str) # 下载完成信号(文件路径)
error_occurred = pyqtSignal(str) # 错误信号
def __init__(self, music_id, music_title, music_artist):
super().__init__()
self.music_id = music_id
self.music_title = music_title
self.music_artist = music_artist
self.is_canceled = False
self.session = requests.Session()
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.error_occurred.emit("未获取到有效的播放链接")
return
play_url = music_url_data["data"].get("play_url")
if not play_url:
self.error_occurred.emit("API 未返回直接播放地址play_url")
return
# 2. 准备文件路径
safe_title = "".join([c for c in self.music_title if c.isalnum() or c in ' _-'])
safe_artist = "".join([c for c in self.music_artist if c.isalnum() or c in ' _-'])
self.final_file = os.path.join(AUDIO_SAVE_DIR, f"{safe_artist} - {safe_title}.mp3")
# 3. 检查是否已下载
if os.path.exists(self.final_file):
self.download_complete.emit(self.final_file)
return
# 4. 开始完整下载
response = self.session.get(play_url, stream=True, timeout=30)
response.raise_for_status()
# 获取总文件大小
total_size = int(response.headers.get("content-length", 0))
if total_size == 0:
self.error_occurred.emit("无法获取文件大小,无法下载")
return
# 5. 写入文件
downloaded_size = 0
with open(self.final_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if self.is_canceled: # 检查是否取消下载
if os.path.exists(self.final_file):
os.remove(self.final_file)
return
if chunk:
f.write(chunk)
downloaded_size += len(chunk)
# 计算进度
progress = int((downloaded_size / total_size) * 100)
self.progress_updated.emit(progress)
# 6. 下载完成
if not self.is_canceled and os.path.exists(self.final_file):
self.download_complete.emit(self.final_file)
except Exception as e:
if not self.is_canceled: # 只有非取消的错误才发送信号
self.error_occurred.emit(f"下载错误:{str(e)}")
# 清理不完整文件
if os.path.exists(self.final_file):
try:
os.remove(self.final_file)
except:
pass
def cancel(self):
"""取消下载"""
self.is_canceled = True
# --- 数据加载工作类 ---
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("落日音乐 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_category = "all" # 默认显示全部
# 分类按钮组(用于管理按钮选中状态)
self.category_buttons = {}
self.category_button_group = QButtonGroup(self)
self.category_button_group.setExclusive(True) # 互斥选择
# 初始化 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=5)
# 刷新按钮
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. 中部主内容区(分割窗口:左侧分类+列表 + 右侧播放器)
main_splitter = QSplitter(Qt.Horizontal)
main_splitter.setContentsMargins(10, 5, 10, 10)
# 左侧区域(分类+列表)
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
# 3.1 分类区域(垂直布局的按钮)
self.category_container = QWidget()
self.category_layout = QVBoxLayout(self.category_container)
self.category_layout.setSpacing(5) # 按钮之间的间距
self.category_layout.setContentsMargins(0, 0, 0, 0)
self.category_container.setFixedHeight(200) # 容纳垂直排列的按钮
left_layout.addWidget(self.category_container)
# 初始化分类按钮(垂直排列)
self.init_categories()
# 3.2 音乐列表
self.music_tree = QTreeWidget()
self.music_tree.setColumnCount(4)
self.music_tree.setHeaderLabels(["标题", "艺术家", "分类", "时长"])
# 设置列宽
self.music_tree.setColumnWidth(0, 300) # 标题列
self.music_tree.setColumnWidth(1, 180) # 艺术家列
self.music_tree.setColumnWidth(2, 100) # 分类列
self.music_tree.setColumnWidth(3, 80) # 时长列
# 单选模式 + 双击播放
self.music_tree.setSelectionMode(QTreeWidget.SingleSelection)
self.music_tree.itemDoubleClicked.connect(self.on_music_double_click)
left_layout.addWidget(self.music_tree)
main_splitter.addWidget(left_widget)
# 右侧区域:标签页(播放器 + 公告)
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公告面板 - 以HTML格式显示内容
self.announcement_panel = QTextEdit()
self.announcement_panel.setReadOnly(True)
self.announcement_panel.setStyleSheet("font-size: 14px; padding: 10px;")
# 设置为HTML格式显示模式
self.announcement_panel.setAcceptRichText(True)
self.announcement_panel.document().setHtml("加载中...")
self.right_tab.addTab(self.announcement_panel, "最新公告")
main_splitter.addWidget(self.right_tab)
main_splitter.setSizes([600, 600]) # 左右窗口比例
main_layout.addWidget(main_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_categories(self):
"""初始化分类为垂直排列的按钮,每个按钮单独一行"""
# 清除现有按钮
for btn in self.category_buttons.values():
self.category_button_group.removeButton(btn)
btn.deleteLater()
self.category_buttons.clear()
for category, config in CATEGORY_CONFIG.items():
# 创建按钮
btn = QPushButton(config['name'])
btn.setMinimumHeight(30) # 按钮高度
btn.setCheckable(True) # 可选中
btn.setStyleSheet(f"""
QPushButton {{
background-color: {config['color']};
color: {config['text_color']};
border-radius: 4px;
font-weight: bold;
padding: 5px;
text-align: center;
}}
QPushButton:checked {{
border: 2px solid #3498db; /* 选中状态边框 */
}}
QPushButton:hover {{
opacity: 0.9; /* 鼠标悬停效果 */
}}
""")
# 存储分类标识
btn.setProperty("category", category)
# 连接点击事件
btn.clicked.connect(self.on_category_button_clicked)
# 添加到布局和按钮组
self.category_layout.addWidget(btn)
self.category_button_group.addButton(btn)
self.category_buttons[category] = btn
# 默认选中"全部音乐"
if 'all' in self.category_buttons:
self.category_buttons['all'].setChecked(True)
def on_category_button_clicked(self):
"""处理分类按钮点击事件"""
# 获取点击的按钮
btn = self.sender()
if btn:
# 获取分类标识
self.current_category = btn.property("category")
self.apply_music_filters()
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;
padding: 10px;
border-radius: 5px;
background-color: #f5f5f5;
""")
panel_layout.addWidget(self.music_info_label)
# 2. 播放进度条
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)
# 3. 时间显示(已播放/总时长)
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)
# 4. 播放控制按钮
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"
"核心功能:\n"
"- 音乐分类与搜索\n"
"- 本地音频缓存\n"
"- 完整播放控制\n"
"- 支持HTML格式公告显示"
)
# --- 数据加载相关函数 ---
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.refresh_btn.setEnabled(enabled)
self.music_tree.setEnabled(enabled)
for btn in self.category_buttons.values():
btn.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):
"""标签数据加载完成后的处理(预留)"""
pass # 分类系统已由CATEGORY_CONFIG定义
def on_announcements_loaded(self, announcements_data):
"""公告数据加载完成后以HTML格式显示内容"""
# 确保公告面板使用HTML格式
self.announcement_panel.setAcceptRichText(True)
if not announcements_data or not announcements_data.get("success", False):
self.announcement_panel.setHtml("<p>公告加载失败或暂无公告</p>")
return
# 构建HTML内容
html_content = "<html><body>"
html_content += "<h2 style='color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 10px;'>最新公告</h2>"
announcements = announcements_data["data"]
for i, ann in enumerate(announcements):
time_str = ann.get("time", "未知时间")
content = ann.get("nr", "无公告内容")
# 将公告内容作为HTML处理
html_content += f"""
<div style='margin-bottom: 20px; padding: 10px; {'background-color: #f9f9f9;' if i % 2 == 0 else ''}'>
<p style='color: #7f8c8d; font-size: 12px; margin: 0 0 10px 0;'>{time_str}</p>
<div style='color: #34495e;'>{content}</div>
</div>
"""
html_content += "</body></html>"
# 设置HTML内容到公告面板
self.announcement_panel.setHtml(html_content)
def populate_music_tree(self):
"""将音乐数据填充到 TreeWidget 列表中"""
self.music_tree.clear()
for idx, music in enumerate(self.filtered_music):
# 获取分类配置
category = music.get("category", "other")
cat_config = CATEGORY_CONFIG.get(category, CATEGORY_CONFIG["other"])
# 格式化时长
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([
music.get("title", "未知标题"),
music.get("artist", "未知艺术家"),
cat_config["name"], # 使用美化后的分类名称
duration_str
])
# 设置分类列的颜色
item.setBackground(2, QBrush(QColor(cat_config["color"])))
item.setForeground(2, QBrush(QColor(cat_config["text_color"])))
# 存储完整音乐信息
item.setData(0, Qt.UserRole, music)
# 交替行颜色
if idx % 2 == 0:
for col in [0, 1, 3]:
item.setBackground(col, 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 apply_music_filters(self):
"""应用筛选条件(关键词+分类)"""
self.filtered_music = []
for music in self.all_music:
# 分类筛选
category_match = True
if self.current_category != "all":
category_match = (music.get("category", "") == self.current_category)
# 关键词筛选
keyword_match = True
if self.current_keyword:
keyword = self.current_keyword
title = music.get("title", "").lower()
artist = music.get("artist", "").lower()
keyword_match = (keyword in title) or (keyword in artist)
# 同时满足才加入筛选结果
if category_match and keyword_match:
self.filtered_music.append(music)
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(2): # 只高亮标题和艺术家列
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()
if self.download_thread and self.download_thread.isRunning():
self.download_thread.cancel()
self.download_thread.wait()
# 2. 记录当前播放的音乐信息
self.current_playing_music = music
music_id = music.get("id")
music_title = music.get("title", "未知音乐")
music_artist = music.get("artist", "未知艺术家")
music_category = music.get("category", "other")
# 获取分类名称
cat_name = CATEGORY_CONFIG.get(music_category, CATEGORY_CONFIG["other"])["name"]
# 3. 更新UI显示
self.music_info_label.setText(f"{music_title} - {music_artist}\n{cat_name}")
self.status_bar.showMessage(f"准备下载:{music_title}")
# 4. 创建下载进度对话框
self.download_dialog = QProgressDialog(f"正在下载 {music_title}...", "取消", 0, 100, self)
self.download_dialog.setWindowTitle("下载中")
self.download_dialog.setWindowModality(Qt.WindowModal)
self.download_dialog.canceled.connect(self.cancel_download)
# 5. 启动下载线程
self.download_thread = DownloadThread(music_id, music_title, music_artist)
# 连接信号
self.download_thread.progress_updated.connect(self.download_dialog.setValue)
self.download_thread.download_complete.connect(self.on_download_complete)
self.download_thread.error_occurred.connect(self.on_download_error)
# 启动线程
self.download_thread.start()
def cancel_download(self):
"""取消下载"""
if self.download_thread and self.download_thread.isRunning():
self.download_thread.cancel()
self.status_bar.showMessage("下载已取消")
def on_download_complete(self, file_path):
"""下载完成后播放音乐"""
if hasattr(self, 'download_dialog'):
self.download_dialog.close()
if not os.path.exists(file_path):
self.on_playback_error("下载文件不存在")
return
# 开始播放
self.current_playing_file = file_path
self.start_playback()
def on_download_error(self, error_msg):
"""下载错误处理"""
if hasattr(self, 'download_dialog'):
self.download_dialog.close()
self.on_playback_error(error_msg)
def start_playback(self):
"""开始播放音频"""
if not self.current_playing_file or not os.path.exists(self.current_playing_file):
self.on_playback_error("播放文件不存在")
return
try:
# 加载并播放音频
pygame.mixer.music.load(self.current_playing_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(len(self.filtered_music) > 1)
self.next_btn.setEnabled(len(self.filtered_music) > 1)
self.play_progress.setDisabled(False)
# 获取并显示总时长
total_duration = pygame.mixer.Sound(self.current_playing_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"正在播放:{self.current_playing_music['title']}"
)
except Exception as e:
self.on_playback_error(f"播放失败:{str(e)}")
def on_playback_error(self, error_msg):
"""播放错误的处理"""
# 重置UI
self.play_progress.setDisabled(True)
self.play_pause_btn.setText("播放")
self.play_pause_btn.setEnabled(False)
self.prev_btn.setEnabled(False)
self.next_btn.setEnabled(False)
# 更新状态信息
self.music_info_label.setText(f"播放失败:{error_msg}")
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()
if self.current_playing_music:
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()
if self.current_playing_music:
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)
self.current_playing_file = ""
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 - 0.5: # 留出一点误差空间
self.play_next() # 自动播放下一曲
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()
# 停止下载线程
if hasattr(self, "download_thread") and self.download_thread.isRunning():
self.download_thread.cancel()
self.download_thread.wait()
# 退出 pygame
pygame.mixer.quit()
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_())