python实现的音乐播放器
python实现的音乐播放器
音乐播放器,原来写过一个简陋的例子,可见
https://blog.csdn.net/cnds123/article/details/137874107
那个不能拖动播放进度条上的滑块到新的位置播放。下面介绍的可以。
简单实用的音乐播放器
这个简单实用的音乐播放器,运行界面:
需要安装,PyQt6这个第三方库。
源码如下:
import sys
import os
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QPushButton, QSlider, QHBoxLayout, QVBoxLayout, QFileDialog, QListWidget, QListWidgetItem
)
from PyQt6.QtCore import Qt, QTimer, QUrl
from PyQt6.QtGui import QPixmap, QPainter, QTransform, QPainterPath
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutputclass MP3Player(QWidget):def __init__(self):super().__init__()self.song_list = []self.current_index = -1self.init_ui()self.setup_player()def init_ui(self):self.setWindowTitle("音乐播放器 V1.0.2")self.setGeometry(300, 300, 600, 400)# 控件初始化self.playlist = QListWidget()self.playlist.itemDoubleClicked.connect(self.play_selected)self.play_btn = QPushButton("▶")self.prev_btn = QPushButton("⏮")self.next_btn = QPushButton("⏭")self.stop_btn = QPushButton("⏹")self.slider = QSlider(Qt.Orientation.Horizontal)self.time_label = QLabel("00:00 / 00:00")# 按钮样式btn_style = """QPushButton {font-size: 20px;min-width: 40px;min-height: 40px;border-radius: 20px;background: #666;color: white;}QPushButton:hover { background: #09f; }"""for btn in [self.play_btn, self.prev_btn, self.next_btn, self.stop_btn]:btn.setStyleSheet(btn_style)# 布局control_layout = QHBoxLayout()control_layout.addWidget(self.prev_btn)control_layout.addWidget(self.play_btn)control_layout.addWidget(self.next_btn)control_layout.addWidget(self.stop_btn)main_layout = QVBoxLayout()main_layout.addWidget(self.playlist)main_layout.addWidget(self.slider)main_layout.addWidget(self.time_label)main_layout.addLayout(control_layout)# 功能按钮buttons_layout = QHBoxLayout()# 添加文件按钮add_file_btn = QPushButton("添加文件")add_file_btn.clicked.connect(self.add_files)buttons_layout.addWidget(add_file_btn)# 删除文件按钮delete_file_btn = QPushButton("删除文件")delete_file_btn.clicked.connect(self.delete_file)buttons_layout.addWidget(delete_file_btn)# 帮助按钮help_btn = QPushButton("帮助")help_btn.clicked.connect(self.show_help)buttons_layout.addWidget(help_btn)main_layout.addLayout(buttons_layout)# 添加音量控制self.volume_slider = QSlider(Qt.Orientation.Horizontal)self.volume_slider.setRange(0, 100)self.volume_slider.setValue(70) # 默认音量70%self.volume_slider.valueChanged.connect(self.change_volume)volume_layout = QHBoxLayout()volume_layout.addWidget(QLabel("音量:"))volume_layout.addWidget(self.volume_slider)main_layout.addLayout(volume_layout)self.setLayout(main_layout)# 连接信号self.play_btn.clicked.connect(self.toggle_play)self.prev_btn.clicked.connect(self.play_prev)self.next_btn.clicked.connect(self.play_next)self.stop_btn.clicked.connect(self.stop)self.slider.sliderMoved.connect(self.seek_position)def delete_file(self):# 获取当前选中的项目current_items = self.playlist.selectedItems()if not current_items:return# 逐一删除所选项目for item in current_items:index = self.playlist.row(item)# 如果删除的是正在播放的歌曲,先停止播放if index == self.current_index:self.player.stop()self.current_index = -1# 从列表和界面中删除项目self.playlist.takeItem(index)self.song_list.pop(index)# 如果正在播放的歌曲在被删除的歌曲之后,需要调整索引if index < self.current_index:self.current_index -= 1# 如果删除后列表为空,重置界面if not self.song_list:self.time_label.setText("00:00 / 00:00")self.slider.setValue(0)self.update_play_button(QMediaPlayer.PlaybackState.StoppedState)def show_help(self):# 创建帮助消息help_text = """<h3>音乐播放器使用帮助</h3><p><b>播放控制:</b></p><ul><li>播放/暂停:点击 ▶/⏸ 按钮</li><li>上一首:点击 ⏮ 按钮</li><li>下一首:点击 ⏭ 按钮</li><li>停止:点击 ⏹ 按钮</li></ul><p><b>播放列表:</b></p><ul><li>添加文件:点击"添加文件"按钮</li><li>删除文件:选择文件后点击"删除文件"按钮</li><li>播放指定歌曲:双击列表中的歌曲</li></ul><p><b>其他控制:</b></p><ul><li>调整进度:拖动进度条</li><li>调整音量:拖动音量滑块</li></ul>"""# 导入需要的组件from PyQt6.QtWidgets import QMessageBox# 显示帮助对话框help_dialog = QMessageBox(self)help_dialog.setWindowTitle("帮助")help_dialog.setTextFormat(Qt.TextFormat.RichText)help_dialog.setText(help_text)help_dialog.setIcon(QMessageBox.Icon.Information)help_dialog.exec()def change_volume(self, value):self.audio_output.setVolume(value / 100.0)def setup_player(self):self.player = QMediaPlayer()self.audio_output = QAudioOutput()self.audio_output.setVolume(0.7) # 默认音量设置self.player.setAudioOutput(self.audio_output)# 定时器更新进度self.timer = QTimer()self.timer.timeout.connect(self.update_progress)self.timer.start(1000)# 播放状态变化self.player.playbackStateChanged.connect(self.update_play_button)# 添加媒体结束时的信号连接self.player.mediaStatusChanged.connect(self.handle_media_status_change)# 添加媒体时长变化的信号连接self.player.durationChanged.connect(self.duration_changed)self.player.errorOccurred.connect(self.handle_error)## def handle_media_status_change(self, status):
## # 使用 QTimer.singleShot 来避免潜在的递归调用或信号冲突
## if status == QMediaPlayer.MediaStatus.EndOfMedia:
## QTimer.singleShot(10, self.play_next)def handle_media_status_change(self, status):# 仅当媒体结束且不是暂停状态时处理if status == QMediaPlayer.MediaStatus.EndOfMedia:# 检查是否只有一首歌曲if len(self.song_list) == 1:# 只有一首歌曲时,重置到开始位置而不是尝试播放"下一首"self.player.setPosition(0)self.player.stop()self.update_play_button(QMediaPlayer.PlaybackState.StoppedState)# 重新播放if self.player.position() >= self.player.duration() - 100 and self.player.duration() > 0:self.player.setPosition(0)self.player.play()else:# 多首歌曲时,播放下一首QTimer.singleShot(10, self.play_next)def add_files(self):files, _ = QFileDialog.getOpenFileNames(self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.flac)")for file in files:if file not in self.song_list:self.song_list.append(file)self.playlist.addItem(os.path.basename(file))def play_selected(self, item):self.current_index = self.playlist.row(item)self.play()def duration_changed(self, duration):self.slider.setRange(0, duration)def handle_error(self, error, error_string):print(f"播放器错误: {error_string}")def play(self):if self.current_index < 0 and self.song_list:self.current_index = 0if 0 <= self.current_index < len(self.song_list):# 高亮当前播放的歌曲self.playlist.setCurrentRow(self.current_index)try:file = self.song_list[self.current_index]self.player.setSource(QUrl.fromLocalFile(file))self.player.play()except Exception as e:print(f"播放错误: {e}")def toggle_play(self):if self.player.isPlaying():self.player.pause()else:if self.player.position() == self.player.duration():self.play()else:self.player.play()def update_play_button(self, state):if state == QMediaPlayer.PlaybackState.PlayingState:self.play_btn.setText("⏸")else:self.play_btn.setText("▶")def update_progress(self):duration = self.player.duration()if duration > 0: # 确保时长大于0current = self.player.position()self.slider.setValue(int(current))self.time_label.setText(f"{self.format_time(current)} / {self.format_time(duration)}")def seek_position(self, position):self.player.setPosition(position)def play_prev(self):if self.song_list:self.current_index = (self.current_index - 1) % len(self.song_list)self.play()def play_next(self):if not self.song_list:return# 先停止当前播放self.player.stop()# 然后切换到下一首self.current_index = (self.current_index + 1) % len(self.song_list)# 使用短延迟来确保状态已正确更新QTimer.singleShot(50, self.play)def stop(self):self.player.stop()self.slider.setValue(0)self.time_label.setText("00:00 / 00:00")def format_time(self, ms):seconds = ms // 1000minutes = seconds // 60seconds = seconds % 60return f"{minutes:02d}:{seconds:02d}"if __name__ == "__main__":app = QApplication(sys.argv)player = MP3Player()player.show()sys.exit(app.exec())
专业级别的音乐播放器
下面这个音乐播放器,源码来源于网络,适当修改,转载记录于此。
需要安装PyQt6、python-vlc、mutagen 这3个第三方库。
还需要安装 VLC 播放器(python-vlc 是 VLC 的 Python 绑定,需依赖系统安装的 VLC),访问 VLC 官网Official download of VLC media player, the best Open Source player - VideoLAN ,下载并安装对应系统的版本。否则 程序运行时提示 vlc.dll not found。
运行效果:
源码如下:
import sys
import os
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QPushButton, QSlider, QHBoxLayout, QVBoxLayout, QGridLayout, QFileDialog, QListWidget, QListWidgetItem, QMenu
)
from PyQt6.QtCore import Qt, QTimer, QUrl, QByteArray, pyqtSignal
from PyQt6.QtGui import QPixmap, QPainter, QTransform, QPainterPath, QFont, QColor, QLinearGradient, QBrush, QPen, QCursor, QScreen
from mutagen.mp3 import MP3
from mutagen.id3 import ID3, APIC
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
import vlc
import redef resource_path(relative_path):if hasattr(sys, '_MEIPASS'):return os.path.join(sys._MEIPASS, relative_path)return os.path.join(os.path.abspath("."), relative_path)# 旋转封面控件
class RotatingCover(QLabel):def __init__(self, song_path, default_cover="fm.png"):super().__init__()self.angle = 0self.pixmap = self.load_cover(song_path, default_cover)if self.pixmap.isNull():self.setText("未找到封面")self.setStyleSheet("color: #fff; background: #666; border-radius: 125px; font-size: 20px;")self.timer = QTimer(self)self.timer.timeout.connect(self.rotate)self.timer.start(50) # 旋转速度def load_cover(self, song_path, default_cover):# 1. 尝试同名jpgbase, _ = os.path.splitext(song_path)jpg_path = base + ".jpg"if os.path.exists(jpg_path):return QPixmap(jpg_path)# 2. 尝试MP3内嵌封面try:audio = MP3(song_path, ID3=ID3)for tag in audio.tags.values():if isinstance(tag, APIC):ba = QByteArray(tag.data)pixmap = QPixmap()pixmap.loadFromData(ba)if not pixmap.isNull():return pixmapexcept Exception as e:pass# 3. 默认封面if os.path.exists(default_cover):return QPixmap(default_cover)return QPixmap() # 空pixmapdef rotate(self):if self.pixmap.isNull():returnself.angle = (self.angle + 2) % 360target_size = 250# 只旋转封面cover_scaled = self.pixmap.scaled(target_size, target_size, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)cover_rotated = cover_scaled.transformed(QTransform().rotate(self.angle), Qt.TransformationMode.SmoothTransformation)# 裁剪为圆形cover_circle = QPixmap(target_size, target_size)cover_circle.fill(Qt.GlobalColor.transparent)painter = QPainter(cover_circle)painter.setRenderHint(QPainter.RenderHint.Antialiasing)path = QPainterPath()path.addEllipse(0, 0, target_size, target_size)painter.setClipPath(path)x = (target_size - cover_rotated.width()) // 2y = (target_size - cover_rotated.height()) // 2painter.drawPixmap(x, y, cover_rotated)painter.end()self.setPixmap(cover_circle)def setCover(self, song_path, default_cover="fm.png"):self.pixmap = self.load_cover(song_path, default_cover)class CoverWidget(QWidget):def __init__(self, default_cover="fm.png"):super().__init__()self.setFixedSize(250, 250)self.bg_pixmap = QPixmap(default_cover) if os.path.exists(default_cover) else QPixmap()self.cover_pixmap = QPixmap()self.angle = 0self.timer = QTimer(self)self.timer.timeout.connect(self.rotate)self.timer.start(50)self.rotate()self.default_cover = default_coverdef rotate(self):self.angle = (self.angle + 2) % 360self.update()def setCover(self, song_path):pixmap = self.load_cover(song_path, self.default_cover)self.cover_pixmap = pixmapself.update()def load_cover(self, song_path, default_cover):if not song_path or not os.path.exists(song_path):return QPixmap(default_cover) if os.path.exists(default_cover) else QPixmap()# 1. 尝试同名jpgbase, _ = os.path.splitext(song_path)jpg_path = base + ".jpg"if os.path.exists(jpg_path):return QPixmap(jpg_path)# 2. 尝试MP3内嵌封面try:audio = MP3(song_path, ID3=ID3)for tag in audio.tags.values():if isinstance(tag, APIC):ba = QByteArray(tag.data)pixmap = QPixmap()pixmap.loadFromData(ba)if not pixmap.isNull():return pixmapexcept Exception as e:pass# 3. 默认封面if os.path.exists(default_cover):return QPixmap(default_cover)return QPixmap() # 空pixmapdef paintEvent(self, event):painter = QPainter(self)painter.setRenderHint(QPainter.RenderHint.Antialiasing)# 1. 画旋转后的 fm.pngif not self.bg_pixmap.isNull():bg = self.bg_pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)# 以中心为原点旋转painter.save()painter.translate(self.width() // 2, self.height() // 2)painter.rotate(self.angle)painter.translate(-bg.width() // 2, -bg.height() // 2)painter.drawPixmap(0, 0, bg)painter.restore()# 2. 画旋转后的封面(内切圆,直径130,居中)if not self.cover_pixmap.isNull():size = 130# 1. 先裁剪为正方形src = self.cover_pixmapw, h = src.width(), src.height()if w != h:side = min(w, h)x = (w - side) // 2y = (h - side) // 2src = src.copy(x, y, side, side)# 2. 缩放cover = src.scaled(size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)# 3. 裁剪为圆形cover_circle = QPixmap(size, size)cover_circle.fill(Qt.GlobalColor.transparent)p = QPainter(cover_circle)p.setRenderHint(QPainter.RenderHint.Antialiasing)path = QPainterPath()path.addEllipse(0, 0, size, size)p.setClipPath(path)p.drawPixmap(0, 0, cover)p.end()# 4. 以中心为原点旋转painter.save()painter.translate(self.width() // 2, self.height() // 2)painter.rotate(self.angle)painter.translate(-size // 2, -size // 2)painter.drawPixmap(0, 0, cover_circle)painter.restore()painter.end()# 动态歌词控件(简化版)
class LyricLabel(QWidget):def __init__(self):super().__init__()self.lyrics = [] # [(time, text), ...]self.current_index = 0self.setMinimumHeight(120)self.setStyleSheet("background: transparent;")self.color_main = QColor("#fff")self.color_other = QColor("#aaa")def setDemoText(self, text):# 只显示一行小字号的提示self.lyrics = []self.current_index = 0self.demo_text = textself.update()def setLyrics(self, lyrics):self.lyrics = lyricsself.current_index = 0self.demo_text = Noneself.update()def setCurrentTime(self, cur_time):self.cur_time = cur_time # 记录当前时间idx = 0for i, (t, _) in enumerate(self.lyrics):if cur_time >= t:idx = ielse:breakif idx != self.current_index:self.current_index = idxself.update()else:self.update() # 即使index没变,也要刷新实现平滑def paintEvent(self, event):painter = QPainter(self)painter.setRenderHint(QPainter.RenderHint.Antialiasing)w, h = self.width(), self.height()# 计算要显示的歌词行lines = []for offset in range(-2, 3):idx = self.current_index + offsetif 0 <= idx < len(self.lyrics):lines.append((offset, self.lyrics[idx][1]))total_lines = len(lines)if total_lines == 0:# demo_textif hasattr(self, "demo_text") and self.demo_text:font = QFont("微软雅黑", 13)painter.setFont(font)painter.setPen(QColor("#aaa"))painter.drawText(0, 0, w, h, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, self.demo_text)return# 1. 先确定最大允许的行高max_line_height = h // max(total_lines, 1)# 2. 计算主行最大字号main_text = lines[[o for o, _ in lines].index(0)][1] if any(o == 0 for o, _ in lines) else lines[0][1]if not main_text.strip():main_text = "啊" # 占位符,防止空行导致字号过大main_font_size = max_line_heightwhile main_font_size > 10:font_main = QFont("微软雅黑", main_font_size, QFont.Weight.Bold)painter.setFont(font_main)rect = painter.fontMetrics().boundingRect(main_text)if rect.width() <= w * 0.95 and rect.height() <= max_line_height * 0.95:breakmain_font_size -= 1font_main = QFont("微软雅黑", main_font_size, QFont.Weight.Bold)other_font_size = int(main_font_size * 0.7)font_other = QFont("微软雅黑", other_font_size)painter.setFont(font_main)line_height = max(painter.fontMetrics().height(), int(h / max(total_lines, 1)))# ====== 平滑滚动核心 ======# 当前歌词时间cur_time = Noneif hasattr(self, "parent") and hasattr(self.parent(), "vlc_player"):cur_time = self.parent().vlc_player.get_time() / 1000elif hasattr(self, "cur_time"):cur_time = self.cur_timeelse:cur_time = 0# 当前行和下一行的时间cur_idx = self.current_indexcur_line_time = self.lyrics[cur_idx][0] if self.lyrics else 0next_line_time = self.lyrics[cur_idx+1][0] if (self.lyrics and cur_idx+1 < len(self.lyrics)) else cur_line_time+2# 计算当前行到下一行的进度百分比if next_line_time > cur_line_time:percent = min(max((cur_time - cur_line_time) / (next_line_time - cur_line_time), 0), 1)else:percent = 0# 歌词整体Y轴平滑上移scroll_offset = -percent * line_height# 歌词整体垂直居中start_y = (h - total_lines * line_height) // 2 + scroll_offsetfor i, (offset, text) in enumerate(lines):y = start_y + i * line_height + line_height // 2if offset == 0:painter.setFont(font_main)# ====== 彩虹色高亮 ======grad = QLinearGradient(0, y-line_height//2, w, y+line_height//2)for j in range(7):grad.setColorAt(j/6, QColor.fromHsv(int((j*60 + (cur_time*60)%360)%360), 255, 255))painter.setPen(QPen(QBrush(grad), 0))else:painter.setFont(font_other)painter.setPen(self.color_other)painter.drawText(0, int(y-line_height//2), w, line_height, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, text)class PlaylistWidget(QListWidget):favSong = pyqtSignal(str)def __init__(self, parent=None):super().__init__(parent)self.setStyleSheet("font-size:18px;background:#222;color:#fff;")self.setDragDropMode(QListWidget.DragDropMode.InternalMove)self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)self.customContextMenuRequested.connect(self.show_menu)self.model().rowsMoved.connect(self.on_rows_moved)def show_menu(self, pos):menu = QMenu(self)act_fav = menu.addAction("收藏该歌曲")act_del = menu.addAction("删除选中项")act_clear = menu.addAction("清空列表")action = menu.exec(self.mapToGlobal(pos))if action == act_fav:self.fav_selected()elif action == act_del:self.delete_selected()elif action == act_clear:self.clear_playlist()def delete_selected(self):for item in self.selectedItems():row = self.row(item)self.takeItem(row)if hasattr(self.parent(), "sync_song_list"):self.parent().sync_song_list()def clear_playlist(self):self.clear()if hasattr(self.parent(), "sync_song_list"):self.parent().sync_song_list()def on_rows_moved(self, *args):if hasattr(self.parent(), "sync_song_list"):self.parent().sync_song_list()def fav_selected(self):for item in self.selectedItems():song = item.toolTip()self.favSong.emit(song)class FloatingLyricWindow(QWidget):EDGE_MARGIN = 8 # 边缘判定宽度def __init__(self):super().__init__()self.setWindowFlags(Qt.WindowType.FramelessWindowHint |Qt.WindowType.WindowStaysOnTopHint |Qt.WindowType.Tool)self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)self.setWindowOpacity(0.85) # 半透明self.lyric = FloatingLyricLabel() # 只显示2行layout = QVBoxLayout()layout.setContentsMargins(16, 16, 16, 16)layout.addWidget(self.lyric)self.setLayout(layout)self.resize(800, 100)screen = QApplication.primaryScreen()screen_geometry = screen.geometry()self.move((screen_geometry.width() - self.width()) // 2,screen_geometry.height() - 150)# 拖动和缩放相关self._move_drag = Falseself._move_DragPosition = Noneself._resize_drag = Falseself._resize_dir = Nonedef paintEvent(self, event):painter = QPainter(self)painter.setRenderHint(QPainter.RenderHint.Antialiasing)rect = self.rect()color = QColor(30, 30, 30, 200) # 深色半透明painter.setBrush(color)painter.setPen(Qt.PenStyle.NoPen)painter.drawRoundedRect(rect, 18, 18)def mousePressEvent(self, event):if event.button() == Qt.MouseButton.LeftButton:pos = event.position().toPoint()margin = self.EDGE_MARGINrect = self.rect()# 判断是否在边缘if pos.x() < margin and pos.y() < margin:self._resize_drag = Trueself._resize_dir = 'topleft'elif pos.x() > rect.width() - margin and pos.y() < margin:self._resize_drag = Trueself._resize_dir = 'topright'elif pos.x() < margin and pos.y() > rect.height() - margin:self._resize_drag = Trueself._resize_dir = 'bottomleft'elif pos.x() > rect.width() - margin and pos.y() > rect.height() - margin:self._resize_drag = Trueself._resize_dir = 'bottomright'elif pos.x() < margin:self._resize_drag = Trueself._resize_dir = 'left'elif pos.x() > rect.width() - margin:self._resize_drag = Trueself._resize_dir = 'right'elif pos.y() < margin:self._resize_drag = Trueself._resize_dir = 'top'elif pos.y() > rect.height() - margin:self._resize_drag = Trueself._resize_dir = 'bottom'else:self._move_drag = Trueself._move_DragPosition = event.globalPosition().toPoint() - self.pos()event.accept()def mouseMoveEvent(self, event):if self._move_drag and event.buttons() == Qt.MouseButton.LeftButton:self.move(event.globalPosition().toPoint() - self._move_DragPosition)event.accept()elif self._resize_drag and event.buttons() == Qt.MouseButton.LeftButton:gpos = event.globalPosition().toPoint()geo = self.geometry()minw, minh = 200, 50if self._resize_dir == 'left':diff = gpos.x() - geo.x()neww = geo.width() - diffif neww > minw:geo.setLeft(gpos.x())elif self._resize_dir == 'right':neww = gpos.x() - geo.x()if neww > minw:geo.setWidth(neww)elif self._resize_dir == 'top':diff = gpos.y() - geo.y()newh = geo.height() - diffif newh > minh:geo.setTop(gpos.y())elif self._resize_dir == 'bottom':newh = gpos.y() - geo.y()if newh > minh:geo.setHeight(newh)elif self._resize_dir == 'topleft':diffx = gpos.x() - geo.x()diffy = gpos.y() - geo.y()neww = geo.width() - diffxnewh = geo.height() - diffyif neww > minw:geo.setLeft(gpos.x())if newh > minh:geo.setTop(gpos.y())elif self._resize_dir == 'topright':diffy = gpos.y() - geo.y()neww = gpos.x() - geo.x()newh = geo.height() - diffyif neww > minw:geo.setWidth(neww)if newh > minh:geo.setTop(gpos.y())elif self._resize_dir == 'bottomleft':diffx = gpos.x() - geo.x()neww = geo.width() - diffxnewh = gpos.y() - geo.y()if neww > minw:geo.setLeft(gpos.x())if newh > minh:geo.setHeight(newh)elif self._resize_dir == 'bottomright':neww = gpos.x() - geo.x()newh = gpos.y() - geo.y()if neww > minw:geo.setWidth(neww)if newh > minh:geo.setHeight(newh)self.setGeometry(geo)event.accept()# 无论是否拖动,都要设置光标self.update_cursor(event.position().toPoint())def mouseReleaseEvent(self, event):self._move_drag = Falseself._resize_drag = Falseself._resize_dir = Nonedef setLyrics(self, lyrics):self.lyric.setLyrics(lyrics)def setCurrentTime(self, time):self.lyric.setCurrentTime(time)def update_cursor(self, pos):margin = self.EDGE_MARGINrect = self.rect()if (pos.x() < margin and pos.y() < margin) or (pos.x() > rect.width() - margin and pos.y() > rect.height() - margin):self.setCursor(Qt.CursorShape.SizeFDiagCursor)elif (pos.x() > rect.width() - margin and pos.y() < margin) or (pos.x() < margin and pos.y() > rect.height() - margin):self.setCursor(Qt.CursorShape.SizeBDiagCursor)elif pos.x() < margin or pos.x() > rect.width() - margin:self.setCursor(Qt.CursorShape.SizeHorCursor)elif pos.y() < margin or pos.y() > rect.height() - margin:self.setCursor(Qt.CursorShape.SizeVerCursor)else:self.setCursor(Qt.CursorShape.ArrowCursor)def enterEvent(self, event):self.update_cursor(self.mapFromGlobal(QCursor.pos()))def leaveEvent(self, event):self.setCursor(Qt.CursorShape.ArrowCursor)class FloatingLyricLabel(LyricLabel):def paintEvent(self, event):painter = QPainter(self)painter.setRenderHint(QPainter.RenderHint.Antialiasing)w, h = self.width(), self.height()# 只显示当前行和下一行,且排除空行lines = []idx = self.current_index# 找到当前行及下一个非空行count = 0while idx < len(self.lyrics) and count < 2:text = self.lyrics[idx][1].strip()if text:lines.append(text)count += 1idx += 1# 如果不足两行,补空字符串while len(lines) < 2:lines.append("")if not any(lines):# 没有歌词,显示demo_textif hasattr(self, "demo_text") and self.demo_text:font = QFont("微软雅黑", int(h * 0.35), QFont.Weight.Bold)painter.setFont(font)painter.setPen(QColor("#aaa"))painter.drawText(0, 0, w, h, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, self.demo_text)returnmain_text = lines[0]next_text = lines[1]# 计算主行和下一行的最大字号,兼顾宽度和高度def fit_font_size(text, max_font_size, max_width, max_height, bold=False):font_size = max_font_sizewhile font_size > 10:font = QFont("微软雅黑", font_size, QFont.Weight.Bold if bold else QFont.Weight.Normal)painter.setFont(font)rect = painter.fontMetrics().boundingRect(text)if rect.width() <= max_width * 0.95 and rect.height() <= max_height * 0.95:breakfont_size -= 1return font_size# 预设主行和下一行高度比例main_ratio = 0.58next_ratio = 0.28gap = int(h * 0.08)main_max_height = h * main_rationext_max_height = h * next_ratio# 先用最大高度和宽度分别拟合字号main_font_size = fit_font_size(main_text, int(main_max_height), w, main_max_height, bold=True)next_font_size = fit_font_size(next_text, int(next_max_height), w, next_max_height)font_main = QFont("微软雅黑", main_font_size, QFont.Weight.Bold)font_next = QFont("微软雅黑", next_font_size)# 重新计算行高painter.setFont(font_main)main_line_height = painter.fontMetrics().height()painter.setFont(font_next)next_line_height = painter.fontMetrics().height()total_height = main_line_height + next_line_height + gapstart_y = (h - total_height) // 2# 当前行:彩虹色高亮painter.setFont(font_main)grad = QLinearGradient(0, start_y, w, start_y + main_line_height)cur_time = getattr(self, "cur_time", 0)for j in range(7):grad.setColorAt(j/6, QColor.fromHsv(int((j*60 + (cur_time*60)%360)%360), 255, 255))painter.setPen(QPen(QBrush(grad), 0))painter.drawText(0, int(start_y), w, main_line_height, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, main_text)# 下一行:灰色painter.setFont(font_next)painter.setPen(QColor(180, 180, 180, 180))painter.drawText(0, int(start_y + main_line_height + gap), w, next_line_height, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, next_text)class CustomTitleBar(QWidget):def __init__(self, parent=None):super().__init__(parent)self.setFixedHeight(40)self.setStyleSheet("""background: #222;border-top-left-radius: 18px;border-top-right-radius: 18px;""")layout = QHBoxLayout(self)layout.setContentsMargins(16, 0, 8, 0)self.title = QLabel("Winamp 音乐播放器")self.title.setStyleSheet("color: #fff; font-size: 18px; font-weight: bold;")layout.addWidget(self.title)layout.addStretch()self.btn_min = QPushButton("—")self.btn_min.setFixedSize(32, 32)self.btn_min.setStyleSheet("color:#fff; background:transparent; font-size:20px; border:none;")self.btn_close = QPushButton("×")self.btn_close.setFixedSize(32, 32)self.btn_close.setStyleSheet("color:#fff; background:transparent; font-size:20px; border:none;")layout.addWidget(self.btn_min)layout.addWidget(self.btn_close)self.btn_min.clicked.connect(self.on_min)self.btn_close.clicked.connect(self.on_close)def on_min(self):self.window().showMinimized()def on_close(self):self.window().close()# 支持拖动窗口def mousePressEvent(self, event):if event.button() == Qt.MouseButton.LeftButton:self._drag_pos = event.globalPosition().toPoint() - self.window().frameGeometry().topLeft()event.accept()def mouseMoveEvent(self, event):if event.buttons() == Qt.MouseButton.LeftButton:self.window().move(event.globalPosition().toPoint() - self._drag_pos)event.accept()def paintEvent(self, event):super().paintEvent(event)painter = QPainter(self)painter.setRenderHint(QPainter.RenderHint.Antialiasing)pen = QPen(QColor(17, 17, 17), 2)painter.setPen(pen)y = self.height() + 4 # 向下移5像素painter.drawLine(10, y, self.width() - 10, y)class MP3Player(QWidget):def __init__(self):super().__init__()self.setWindowFlags(Qt.WindowType.FramelessWindowHint)self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)self.song_list = [] # 歌曲文件路径列表self.current_index = -1 # 当前播放索引self.player = QMediaPlayer()self.audio_output = QAudioOutput()self.player.setAudioOutput(self.audio_output)self.vlc_player = vlc.MediaPlayer()self.timer = QTimer(self)self.timer.setInterval(500)self.timer.timeout.connect(self.update_progress)self.slider_is_pressed = Falseself.loop_mode = False # False: 顺序播放, True: 随机播放self.shuffle_mode = False # False: 顺序播放, True: 随机播放#self.default_lyric_text = "深埋生命血脉相连\n用丝绸去润泽你的肌肤"self.default_lyric_text = "未检测到歌词\n正在播放..."self.floating_lyric = FloatingLyricWindow()self.floating_lyric_visible = Falseself.is_muted = False#self.btn_mute = QPushButton("🔊")self.btn_mute = QPushButton("🔊")self.btn_mute.setFixedSize(48, 48)self.btn_mute.setStyleSheet("font-size: 24px; background: #333; color: #fff; border-radius: 24px; font-weight: bold;")self.btn_mute.clicked.connect(self.toggle_mute)self.initUI()self.load_last_playlist()def initUI(self):self.song_path = "test.mp3" # 默认MP3文件路径self.cover = CoverWidget(resource_path("fm.png"))self.lyric = LyricLabel()self.lyric.setDemoText(self.default_lyric_text)first_row = QHBoxLayout()first_row.addWidget(self.cover, 1)first_row.addWidget(self.lyric, 2)# 第二行self.slider = QSlider(Qt.Orientation.Horizontal)self.slider.setRange(0, 100)self.slider.setValue(0)self.time_label_left = QLabel("00:00")self.time_label_right = QLabel("05:27")self.time_label_left.setStyleSheet("color: #aaa;")self.time_label_right.setStyleSheet("color: #aaa;")second_row = QHBoxLayout()second_row.addWidget(self.time_label_left)second_row.addWidget(self.slider, 1)second_row.addWidget(self.time_label_right)# 第三行
## self.btn_prev = QPushButton("⏮")
## self.btn_play = QPushButton("▶")
## self.btn_next = QPushButton("⏭")
## self.btn_loop = QPushButton("🔁")
## self.btn_stop = QPushButton("⏹")self.btn_prev = QPushButton("⏮")self.btn_play = QPushButton("▶")self.btn_next = QPushButton("⏭")self.btn_loop = QPushButton("🔁;")self.btn_stop = QPushButton("⏹") self.btn_add_file = QPushButton("添加文件")self.btn_add_dir = QPushButton("添加目录")self.btn_save_list = QPushButton("保存列表")self.btn_load_list = QPushButton("加载列表")#self.btn_mute = QPushButton("🔊")self.btn_mute = QPushButton("🔊")self.btn_float_lyric = QPushButton("词") button_style = """QPushButton {font-size: 24px;background: #333;color: #fff;border-radius: 24px;font-weight: bold;border: none;}QPushButton:hover {background: #09f;color: #fff;}"""for btn in [self.btn_prev, self.btn_play, self.btn_next, self.btn_loop, self.btn_stop, self.btn_mute, self.btn_float_lyric]:btn.setFixedSize(48, 48)btn.setStyleSheet(button_style)list_button_style = """QPushButton {font-size: 16px;background: #555;color: #fff;border-radius: 8px;padding: 6px 18px;border: none;}QPushButton:hover {background: #09f;color: #fff;}"""self.btn_add_file.setFixedSize(100, 36)self.btn_add_dir.setFixedSize(100, 36)self.btn_save_list.setFixedSize(100, 36)self.btn_load_list.setFixedSize(100, 36)self.btn_add_file.setStyleSheet(list_button_style)self.btn_add_dir.setStyleSheet(list_button_style)self.btn_save_list.setStyleSheet(list_button_style)self.btn_load_list.setStyleSheet(list_button_style)self.btn_add_file.clicked.connect(self.open_file)self.btn_add_dir.clicked.connect(self.open_dir)self.btn_save_list.clicked.connect(self.save_playlist)self.btn_load_list.clicked.connect(self.load_playlist)self.btn_prev.clicked.connect(self.play_prev)self.btn_next.clicked.connect(self.play_next)# self.btn_play.clicked.connect(self.play_selected)self.btn_play.clicked.connect(self.toggle_play_pause)self.btn_loop.clicked.connect(self.toggle_shuffle_mode)self.btn_stop.clicked.connect(self.stop_play)self.btn_mute.clicked.connect(self.toggle_mute)self.btn_float_lyric.clicked.connect(self.toggle_floating_lyric)third_row = QHBoxLayout()third_row.setSpacing(8) # 设置按钮间距third_row.addStretch()for btn in [self.btn_prev, self.btn_play, self.btn_next, self.btn_loop, self.btn_stop, self.btn_mute, self.btn_float_lyric]:third_row.addWidget(btn)third_row.addSpacing(20)third_row.addWidget(self.btn_add_file)third_row.addWidget(self.btn_add_dir)third_row.addWidget(self.btn_save_list)third_row.addWidget(self.btn_load_list)third_row.addStretch()# 新增:播放列表self.list_widget = PlaylistWidget(self)self.list_widget.itemDoubleClicked.connect(self.on_item_double_clicked)self.list_widget.favSong.connect(self.append_to_fav)# 总体布局main_widget = QWidget(self)main_widget.setObjectName("main_widget")main_widget.setStyleSheet("""#main_widget {background: #222;border-radius: 18px;}""")main_layout = QVBoxLayout(main_widget)main_layout.setContentsMargins(0, 0, 0, 0)main_layout.setSpacing(0)self.title_bar = CustomTitleBar(self)main_layout.addWidget(self.title_bar)content_layout = QVBoxLayout()content_layout.addLayout(first_row, 3)content_layout.addLayout(second_row, 1)content_layout.addLayout(third_row, 1)content_layout.addWidget(self.list_widget, 2)content_layout.setContentsMargins(16, 0, 16, 16)main_layout.addLayout(content_layout)layout = QVBoxLayout(self)layout.addWidget(main_widget)layout.setContentsMargins(0, 0, 0, 0)self.resize(1000, 700)self.slider.sliderPressed.connect(self.on_slider_pressed)self.slider.sliderReleased.connect(self.on_slider_released)self.slider.sliderMoved.connect(self.on_slider_moved)self.update_shuffle_button_style()print(f"初始按钮文本: {self.btn_loop.text()}")# 添加滚动条样式scrollbar_style = """QScrollBar:vertical {background: #222;width: 14px;margin: 4px 2px 4px 2px;border-radius: 7px;}QScrollBar::handle:vertical {background: #444;min-height: 40px;border-radius: 7px;}QScrollBar::handle:vertical:hover {background: #09f;}QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {background: none;height: 0px;}QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {background: none;}QScrollBar:horizontal {background: #222;height: 14px;margin: 2px 4px 2px 4px;border-radius: 7px;}QScrollBar::handle:horizontal {background: #444;min-width: 40px;border-radius: 7px;}QScrollBar::handle:horizontal:hover {background: #09f;}QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {background: none;width: 0px;}QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {background: none;}"""self.list_widget.verticalScrollBar().setStyleSheet(scrollbar_style)self.list_widget.horizontalScrollBar().setStyleSheet(scrollbar_style)self.load_last_playlist()def open_file(self):files, _ = QFileDialog.getOpenFileNames(self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.flac);;所有文件 (*)")for file in files:self.add_song(file)def open_dir(self):dir_path = QFileDialog.getExistingDirectory(self, "选择文件夹", "")if dir_path:for root, dirs, files in os.walk(dir_path):for fname in files:if fname.lower().endswith(('.mp3', '.wav', '.flac')):self.add_song(os.path.join(root, fname))def add_song(self, file):if file not in self.song_list:self.song_list.append(file)item = QListWidgetItem(os.path.basename(file))item.setToolTip(file)self.list_widget.addItem(item)# 如果是第一首,自动选中if len(self.song_list) == 1:self.list_widget.setCurrentRow(0)self.play_selected()def on_item_double_clicked(self, item):row = self.list_widget.row(item)self.sync_song_list()self.current_index = rowself.play_song_by_index(row)def play_selected(self):"""播放或暂停当前选中的歌曲"""row = self.list_widget.currentRow()self.sync_song_list()if row >= 0:self.current_index = rowstate = self.vlc_player.get_state()if state in (vlc.State.Playing, vlc.State.Buffering):# 如果正在播放,则暂停self.vlc_player.pause()#self.btn_play.setText("▶") # 显示播放图标self.btn_play.setText("▶")elif state == vlc.State.Paused:# 如果暂停中,则继续播放self.vlc_player.play()#self.btn_play.setText("⏸") # 显示暂停图标self.btn_play.setText("⏸") elif state in (vlc.State.Stopped, vlc.State.Ended, vlc.State.NothingSpecial):# 如果停止或未开始,则播放选中歌曲self.play_song_by_index(row)def pause_play(self):"""暂停当前播放"""if self.vlc_player.is_playing():self.vlc_player.pause()#self.btn_play.setText("▶") # 显示播放图标self.btn_play.setText("▶")def resume_play(self):"""继续播放"""if self.vlc_player.get_state() == vlc.State.Paused:self.vlc_player.play()#self.btn_play.setText("⏸") # 显示暂停图标self.btn_play.setText("⏸")def toggle_play_pause(self):"""切换播放/暂停状态"""if not self.song_list:# 如果播放列表为空,则提示用户添加歌曲print("播放列表为空,请先添加歌曲")returnif self.vlc_player.is_playing():self.pause_play()elif self.vlc_player.get_state() == vlc.State.Paused:self.resume_play()else:# 如果没有正在播放的歌曲,则播放当前选中的歌曲row = self.list_widget.currentRow()if row >= 0:self.play_song_by_index(row)else:# 如果没有选中歌曲,则播放第一首if self.song_list:self.list_widget.setCurrentRow(0)self.play_song_by_index(0)def keyPressEvent(self, event):"""处理键盘事件"""if event.key() == Qt.Key.Key_Space:# 空格键控制播放/暂停self.toggle_play_pause()elif event.key() == Qt.Key.Key_Left:# 左箭头键播放上一首self.play_prev()elif event.key() == Qt.Key.Key_Right:# 右箭头键播放下一首self.play_next()elif event.key() == Qt.Key.Key_Escape:# ESC键关闭窗口self.close()else:super().keyPressEvent(event)def play_song_by_index(self, idx):if 0 <= idx < len(self.song_list):self.current_index = idxsong_path = self.song_list[idx]song_name = os.path.basename(song_path) # 获取文件名song_name_without_ext = os.path.splitext(song_name)[0] # 去除扩展名self.cover.setCover(song_path)self.list_widget.setCurrentRow(idx)self.vlc_player.stop()self.vlc_player.set_media(vlc.Media(song_path))self.vlc_player.play()self.timer.start()self.slider.setValue(0)self.time_label_left.setText("00:00")self.time_label_right.setText("--:--")# 更新标题显示当前播放的歌曲名self.title_bar.title.setText(f"正在播放: {song_name_without_ext}")# 使用自定义的默认歌词文本,包含歌曲名custom_default_text = f"正在播放: {song_name_without_ext}\n无歌词可用"print(f"播放:{song_path}")# 加载歌词try:lrc = self.load_lrc(song_path) or self.load_embedded_lyric(song_path)if lrc:parsed = parse_lrc(lrc)if parsed:self.lyrics_parsed = parsedself.lyric.setLyrics(self.lyrics_parsed)self.floating_lyric.setLyrics(self.lyrics_parsed) # 同步到浮动歌词else:self.lyric.setDemoText(custom_default_text)self.floating_lyric.setLyrics([])else:self.lyric.setDemoText(custom_default_text)self.floating_lyric.setLyrics([])except Exception as e:self.lyric.setDemoText(custom_default_text)self.floating_lyric.setLyrics([])def play_prev(self):if self.song_list:if self.shuffle_mode:# 随机模式:随机选择一首(避免当前曲目)import randomcandidates = [i for i in range(len(self.song_list)) if i != self.current_index]if candidates:self.current_index = random.choice(candidates)else:self.current_index = self.current_indexelse:# 顺序模式:播放上一首self.current_index = (self.current_index - 1) % len(self.song_list)self.play_song_by_index(self.current_index)def play_next(self):if self.song_list:if self.shuffle_mode:# 随机模式:随机选择一首(避免当前曲目)import randomcandidates = [i for i in range(len(self.song_list)) if i != self.current_index]if candidates:self.current_index = random.choice(candidates)else:self.current_index = self.current_indexelse:# 顺序模式:播放下一首self.current_index = (self.current_index + 1) % len(self.song_list)self.play_song_by_index(self.current_index)def on_slider_pressed(self):self.slider_is_pressed = Truedef on_slider_released(self):self.slider_is_pressed = Falsetotal = self.vlc_player.get_length()if total > 0:pos = self.slider.value() / 100self.vlc_player.set_time(int(total * pos))def on_slider_moved(self, value):total = self.vlc_player.get_length()if total > 0:cur_time = int(total * value / 100)self.time_label_left.setText(self.ms_to_mmss(cur_time))def update_progress(self):if self.vlc_player.is_playing() and not self.slider_is_pressed:total = self.vlc_player.get_length()cur = self.vlc_player.get_time()if total > 0:percent = int(cur / total * 100)self.slider.setValue(percent)self.time_label_left.setText(self.ms_to_mmss(cur))self.time_label_right.setText(self.ms_to_mmss(total))else:self.slider.setValue(0)self.time_label_right.setText("--:--")elif not self.vlc_player.is_playing():if self.vlc_player.get_state() == vlc.State.Ended:if self.shuffle_mode:import randomif self.song_list:# 避免重复播放当前曲目candidates = [i for i in range(len(self.song_list)) if i != self.current_index]if candidates:next_index = random.choice(candidates)else:next_index = self.current_indexself.play_song_by_index(next_index)else:self.play_next()# 图标联动state = self.vlc_player.get_state()if state in (vlc.State.Playing, vlc.State.Buffering):#self.btn_play.setText("⏸")self.btn_play.setText("⏸")else:#self.btn_play.setText("▶")self.btn_play.setText("▶")if hasattr(self, "lyric") and hasattr(self, "lyrics_parsed"):cur = self.vlc_player.get_time() / 1000self.lyric.setCurrentTime(cur)self.floating_lyric.setCurrentTime(cur) # 同步到浮动歌词def ms_to_mmss(self, ms):s = int(ms // 1000)m = s // 60s = s % 60return f"{m:02d}:{s:02d}"def save_playlist(self):self.sync_song_list()file_path, _ = QFileDialog.getSaveFileName(self, "保存播放列表", "", "播放列表文件 (*.m3u *.txt);;所有文件 (*)")if file_path:try:with open(file_path, "w", encoding="utf-8") as f:for song in self.song_list:f.write(song + "\n")except Exception as e:print("保存失败:", e)def load_playlist(self):file_path, _ = QFileDialog.getOpenFileName(self, "加载播放列表", "", "播放列表文件 (*.m3u *.txt);;所有文件 (*)")if file_path:try:with open(file_path, "r", encoding="utf-8") as f:lines = [line.strip() for line in f if line.strip()]self.song_list.clear()self.list_widget.clear()for song in lines:if os.path.exists(song):self.song_list.append(song)item = QListWidgetItem(os.path.basename(song))item.setToolTip(song)self.list_widget.addItem(item)if self.song_list:self.list_widget.setCurrentRow(0)self.play_song_by_index(0)# 保存最后一次歌单路径with open("last_playlist.txt", "w", encoding="utf-8") as f:f.write(file_path)except Exception as e:print("加载失败:", e)def sync_song_list(self):self.song_list = []for i in range(self.list_widget.count()):item = self.list_widget.item(i)if item and item.toolTip():self.song_list.append(item.toolTip())def toggle_shuffle_mode(self):self.shuffle_mode = not self.shuffle_modeself.update_shuffle_button_style()print(f"切换后模式: {self.shuffle_mode}, 按钮文本: {self.btn_loop.text()}")def stop_play(self):self.vlc_player.stop()self.timer.stop()self.slider.setValue(0)self.time_label_left.setText("00:00")self.time_label_right.setText("--:--")self.lyric.setDemoText(self.default_lyric_text)if self.song_list and 0 <= self.current_index < len(self.song_list):self.append_to_fav(self.song_list[self.current_index])def update_shuffle_button_style(self):if self.shuffle_mode:#self.btn_loop.setText("🔀")self.btn_loop.setText("🔄")else:#self.btn_loop.setText("🔁")self.btn_loop.setText("🔁")self.btn_loop.update() # 强制刷新按钮def load_lrc(self, song_path):lrc_path = os.path.splitext(song_path)[0] + ".lrc"if os.path.exists(lrc_path):with open(lrc_path, encoding="utf-8") as f:return f.read()return Nonedef load_embedded_lyric(self, song_path):try:audio = MP3(song_path, ID3=ID3)for tag in audio.tags.values():if tag.FrameID == "USLT":return tag.textexcept Exception:passreturn Nonedef toggle_floating_lyric(self):if self.floating_lyric_visible:self.floating_lyric.hide()self.floating_lyric_visible = Falseself.btn_float_lyric.setStyleSheet("font-size: 24px; background: #333; color: #fff; border-radius: 24px; font-weight: bold;")else:self.floating_lyric.show()self.floating_lyric_visible = Trueself.btn_float_lyric.setStyleSheet("font-size: 24px; background: #09f; color: #fff; border-radius: 24px; font-weight: bold;")def toggle_mute(self):self.is_muted = not self.is_mutedself.vlc_player.audio_set_mute(self.is_muted)if self.is_muted:#self.btn_mute.setText("🔇")self.btn_mute.setText("🔇")self.btn_mute.setStyleSheet("font-size: 24px; background: #09f; color: #fff; border-radius: 24px; font-weight: bold;")else:#self.btn_mute.setText("🔊") self.btn_mute.setText("🔊") self.btn_mute.setStyleSheet("font-size: 24px; background: #333; color: #fff; border-radius: 24px; font-weight: bold;")def append_to_fav(self, song):fav_path = os.path.abspath("收藏歌单.m3u")try:need_header = not os.path.exists(fav_path)if os.path.exists(fav_path):with open(fav_path, "r", encoding="utf-8") as f:lines = [line.strip() for line in f if line.strip()]if song in lines:returnwith open(fav_path, "a", encoding="utf-8") as f:if need_header:f.write("#EXTM3U\n")f.write(song + "\n")print(f"已收藏到: {fav_path}") # 调试用except Exception as e:print("收藏失败:", e)def load_last_playlist(self):try:if os.path.exists("last_playlist.txt"):with open("last_playlist.txt", "r", encoding="utf-8") as f:file_path = f.read().strip()if file_path and os.path.exists(file_path):with open(file_path, "r", encoding="utf-8") as f:lines = [line.strip() for line in f if line.strip()]self.song_list.clear()self.list_widget.clear()for song in lines:if os.path.exists(song):self.song_list.append(song)item = QListWidgetItem(os.path.basename(song))item.setToolTip(song)self.list_widget.addItem(item)if self.song_list:self.list_widget.setCurrentRow(0)self.play_song_by_index(0)except Exception as e:print("自动加载上次歌单失败:", e)def parse_lrc(lrc_text):pattern = re.compile(r"\[(\d+):(\d+)(?:\.(\d+))?\](.*)")result = []for line in lrc_text.splitlines():m = pattern.match(line)if m:minute = int(m.group(1))second = int(m.group(2))ms = int(m.group(3) or 0)time = minute * 60 + second + ms / 100 if ms else minute * 60 + secondtext = m.group(4).strip()result.append((time, text))result.sort()return resultif __name__ == "__main__":try:app = QApplication(sys.argv)player = MP3Player()player.show()print("当前工作目录:", os.getcwd())sys.exit(app.exec()) # PyQt6中exec_()改为exec()except Exception as e:print(f"程序发生错误: {e}")import tracebacktraceback.print_exc()
OK!