diff --git a/Beta-0.1.4.py b/Beta-0.1.4.py new file mode 100644 index 0000000..a8a0782 --- /dev/null +++ b/Beta-0.1.4.py @@ -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("
公告加载失败或暂无公告
") + return + + # 构建HTML内容 + html_content = "" + html_content += "{time_str}
+