上传文件至 /
Beta0.1
This commit is contained in:
283
Beta-0.1.1.py
Normal file
283
Beta-0.1.1.py
Normal file
@@ -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_())
|
||||
725
Beta-0.1.2.py
Normal file
725
Beta-0.1.2.py
Normal file
@@ -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_())
|
||||
Reference in New Issue
Block a user