303 lines
13 KiB
Python
303 lines
13 KiB
Python
import sys
|
||
import os
|
||
import pygame
|
||
import time
|
||
import re
|
||
from PyQt6.QtWidgets import (
|
||
QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel,
|
||
QPushButton, QSlider, QHBoxLayout, QFileDialog
|
||
)
|
||
from PyQt6.QtCore import QTimer, Qt, QSize, QPropertyAnimation, QPoint
|
||
from PyQt6.QtGui import QPixmap, QIcon, QPalette, QColor
|
||
|
||
class MusicPlayer(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("DreamLife|MusicPlayer")
|
||
self.setGeometry(300, 200, 400, 500)
|
||
self.initUI()
|
||
pygame.mixer.init()
|
||
|
||
self.playlist = [] # 歌曲列表(文件路径)
|
||
self.current_song_index = -1 # 当前播放歌曲索引
|
||
self.lyrics = [] # 解析后的歌词列表 [(时间戳, 歌词)]
|
||
self.current_lyric_index = 0 # 当前歌词索引
|
||
self.is_playing = False # 播放状态
|
||
self.music_length = 0 # 音乐总时长(秒)
|
||
self.start_time = 0 # 播放开始时间(用于计算已播放时间)
|
||
self.pause_time = 0 # 暂停时记录的播放进度(秒)
|
||
self.animation = None # 存储滚动动画对象
|
||
self.last_lyric = None # 上一次显示的歌词,用于避免重复启动动画
|
||
self.music_file = None # 当前音乐文件路径
|
||
self.lyrics_file = None # 当前歌词文件路径
|
||
self.cover_file = None # 当前封面文件路径
|
||
|
||
def initUI(self):
|
||
self.central_widget = QWidget()
|
||
self.setCentralWidget(self.central_widget)
|
||
self.layout = QVBoxLayout()
|
||
self.layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.central_widget.setLayout(self.layout)
|
||
|
||
# 设置背景颜色
|
||
self.set_background_color("#2E2E2E")
|
||
|
||
# 封面
|
||
self.cover_label = QLabel()
|
||
self.layout.addWidget(self.cover_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||
|
||
# 添加一个按钮用于加载外部音乐文件夹
|
||
self.load_folder_button = QPushButton("选择音乐文件夹")
|
||
self.load_folder_button.clicked.connect(self.open_folder_dialog)
|
||
self.layout.addWidget(self.load_folder_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||
|
||
# 歌词显示区域(固定大小容器,便于动画处理,不随内容改变大小)
|
||
self.lyrics_container = QWidget()
|
||
self.lyrics_container.setFixedSize(300, 50)
|
||
self.lyrics_container.setStyleSheet("background-color: transparent;")
|
||
# 歌词标签放置在容器中
|
||
self.lyrics_browser = QLabel("", self.lyrics_container)
|
||
self.lyrics_browser.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.lyrics_browser.setStyleSheet("font-size: 24px; color: white; font-weight: bold;")
|
||
self.lyrics_browser.setFixedHeight(50)
|
||
self.lyrics_browser.move(0, 0)
|
||
self.layout.addWidget(self.lyrics_container, alignment=Qt.AlignmentFlag.AlignCenter)
|
||
|
||
# 进度条
|
||
self.slider = QSlider(Qt.Orientation.Horizontal)
|
||
self.slider.setRange(0, 100)
|
||
self.slider.sliderPressed.connect(self.pause_music)
|
||
self.slider.sliderReleased.connect(self.seek_music)
|
||
self.layout.addWidget(self.slider)
|
||
|
||
# 播放控制区域(增加上一首、播放/暂停、下一首)
|
||
self.control_layout = QHBoxLayout()
|
||
self.control_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.prev_button = QPushButton()
|
||
self.prev_button.setIcon(QIcon(self.resource_path("file/prev.png")))
|
||
self.prev_button.setIconSize(QSize(40, 40))
|
||
self.prev_button.setStyleSheet("border: none;")
|
||
self.prev_button.clicked.connect(self.play_previous_song)
|
||
self.control_layout.addWidget(self.prev_button)
|
||
|
||
self.play_button = QPushButton()
|
||
self.play_button.setIcon(QIcon(self.resource_path("file/play.png")))
|
||
self.play_button.setIconSize(QSize(52, 52))
|
||
self.play_button.setStyleSheet("border: none;")
|
||
self.play_button.clicked.connect(self.toggle_play_pause)
|
||
self.control_layout.addWidget(self.play_button)
|
||
|
||
self.next_button = QPushButton()
|
||
self.next_button.setIcon(QIcon(self.resource_path("file/next.png")))
|
||
self.next_button.setIconSize(QSize(40, 40))
|
||
self.next_button.setStyleSheet("border: none;")
|
||
self.next_button.clicked.connect(self.play_next_song)
|
||
self.control_layout.addWidget(self.next_button)
|
||
self.layout.addLayout(self.control_layout)
|
||
|
||
# 定时器,每500毫秒更新一次进度和歌词
|
||
self.timer = QTimer(self)
|
||
self.timer.timeout.connect(self.update_lyrics_and_progress)
|
||
|
||
def resource_path(self, relative_path):
|
||
""" 获取资源文件的路径,适配 PyInstaller 打包模式 """
|
||
if getattr(sys, 'frozen', False):
|
||
base_path = sys._MEIPASS # PyInstaller 运行时解压目录
|
||
else:
|
||
base_path = os.path.dirname(os.path.abspath(__file__)) # 开发模式
|
||
return os.path.join(base_path, relative_path)
|
||
|
||
def set_background_color(self, color):
|
||
palette = self.palette()
|
||
palette.setColor(QPalette.ColorRole.Window, QColor(color))
|
||
self.setPalette(palette)
|
||
self.central_widget.setStyleSheet(f"background-color: {color};")
|
||
|
||
def load_cover(self):
|
||
# 如果用户选择了封面文件,优先加载,否则加载默认封面
|
||
if self.cover_file and os.path.exists(self.cover_file):
|
||
pixmap = QPixmap(self.cover_file)
|
||
else:
|
||
cover_path = self.resource_path("file/cover.jpg")
|
||
if os.path.exists(cover_path):
|
||
pixmap = QPixmap(cover_path)
|
||
else:
|
||
self.cover_label.setText("封面未找到")
|
||
return
|
||
self.cover_label.setPixmap(pixmap.scaled(256, 256, Qt.AspectRatioMode.KeepAspectRatio))
|
||
|
||
def load_lyrics(self):
|
||
# 根据用户选择的歌词文件加载歌词,如果没有选择则使用默认歌词文件
|
||
if self.lyrics_file and os.path.exists(self.lyrics_file):
|
||
path = self.lyrics_file
|
||
else:
|
||
# 默认歌词文件(可以根据需要调整)
|
||
path = self.resource_path("file/默认歌词.lrc")
|
||
if os.path.exists(path):
|
||
try:
|
||
with open(path, 'r', encoding='utf-8') as file:
|
||
lines = file.readlines()
|
||
self.lyrics = []
|
||
for line in lines:
|
||
match = re.match(r"\[(\d+):(\d+\.\d+)](.*)", line)
|
||
if match:
|
||
minutes = int(match.group(1))
|
||
seconds = float(match.group(2))
|
||
timestamp = minutes * 60 + seconds
|
||
lyrics_text = match.group(3).strip()
|
||
self.lyrics.append((timestamp, lyrics_text))
|
||
self.lyrics.sort()
|
||
except Exception as e:
|
||
self.lyrics_browser.setText("加载歌词出错: " + str(e))
|
||
else:
|
||
self.lyrics_browser.setText("歌词文件未找到")
|
||
|
||
def open_folder_dialog(self):
|
||
folder_path = QFileDialog.getExistingDirectory(self, "选择音乐文件夹", "")
|
||
if folder_path:
|
||
# 读取该文件夹下所有音乐文件(支持wav和mp3)
|
||
self.playlist = []
|
||
for file in os.listdir(folder_path):
|
||
if file.lower().endswith(('.wav', '.mp3')):
|
||
self.playlist.append(os.path.join(folder_path, file))
|
||
self.playlist.sort() # 可按文件名排序
|
||
if self.playlist:
|
||
# 将当前索引设置为第一首
|
||
self.current_song_index = 0
|
||
self.load_current_song_resources()
|
||
self.play_music()
|
||
else:
|
||
self.lyrics_browser.setText("文件夹中没有找到音乐文件")
|
||
|
||
def load_current_song_resources(self):
|
||
# 设置当前音乐文件路径及相关资源(歌词和封面)
|
||
self.music_file = self.playlist[self.current_song_index]
|
||
base, _ = os.path.splitext(self.music_file)
|
||
lyrics_candidate = base + ".lrc"
|
||
cover_candidate = base + ".jpg"
|
||
self.lyrics_file = lyrics_candidate if os.path.exists(lyrics_candidate) else None
|
||
self.cover_file = cover_candidate if os.path.exists(cover_candidate) else None
|
||
self.load_cover()
|
||
self.load_lyrics()
|
||
|
||
def toggle_play_pause(self):
|
||
if not self.is_playing:
|
||
if self.pause_time > 0:
|
||
self.resume_music()
|
||
else:
|
||
self.play_music()
|
||
else:
|
||
self.pause_music()
|
||
|
||
def play_music(self):
|
||
if self.music_file and os.path.exists(self.music_file):
|
||
try:
|
||
pygame.mixer.music.load(self.music_file)
|
||
pygame.mixer.music.play(start=0)
|
||
self.music_length = pygame.mixer.Sound(self.music_file).get_length()
|
||
self.start_time = time.time()
|
||
self.pause_time = 0
|
||
self.timer.start(500)
|
||
self.play_button.setIcon(QIcon(self.resource_path("file/pause.png")))
|
||
self.is_playing = True
|
||
except Exception as e:
|
||
self.lyrics_browser.setText("播放错误: " + str(e))
|
||
else:
|
||
self.lyrics_browser.setText("")
|
||
|
||
def resume_music(self):
|
||
try:
|
||
pygame.mixer.music.unpause()
|
||
self.start_time = time.time() - self.pause_time
|
||
self.timer.start(500)
|
||
self.play_button.setIcon(QIcon(self.resource_path("file/pause.png")))
|
||
self.is_playing = True
|
||
except Exception as e:
|
||
self.lyrics_browser.setText("恢复播放错误: " + str(e))
|
||
|
||
def pause_music(self):
|
||
if self.is_playing:
|
||
self.pause_time = time.time() - self.start_time
|
||
pygame.mixer.music.pause()
|
||
self.timer.stop()
|
||
self.play_button.setIcon(QIcon(self.resource_path("file/play.png")))
|
||
self.is_playing = False
|
||
|
||
def seek_music(self):
|
||
if self.music_length > 0:
|
||
new_time = self.slider.value() / 100 * self.music_length
|
||
pygame.mixer.music.play(start=new_time)
|
||
self.start_time = time.time() - new_time
|
||
self.pause_time = new_time
|
||
self.current_lyric_index = self.find_lyric_index(new_time)
|
||
self.update_lyrics_display()
|
||
self.timer.start(500)
|
||
self.play_button.setIcon(QIcon(self.resource_path("file/pause.png")))
|
||
self.is_playing = True
|
||
|
||
def play_next_song(self):
|
||
if self.playlist:
|
||
self.current_song_index = (self.current_song_index + 1) % len(self.playlist)
|
||
self.load_current_song_resources()
|
||
self.play_music()
|
||
|
||
def play_previous_song(self):
|
||
if self.playlist:
|
||
self.current_song_index = (self.current_song_index - 1) % len(self.playlist)
|
||
self.load_current_song_resources()
|
||
self.play_music()
|
||
|
||
def find_lyric_index(self, current_time):
|
||
for i, (timestamp, _) in enumerate(self.lyrics):
|
||
if current_time < timestamp:
|
||
return max(i - 1, 0)
|
||
return len(self.lyrics) - 1
|
||
|
||
def update_lyrics_display(self):
|
||
if not self.lyrics:
|
||
self.lyrics_browser.setText("歌词为空")
|
||
return
|
||
if self.current_lyric_index < len(self.lyrics):
|
||
_, lyric = self.lyrics[self.current_lyric_index]
|
||
if self.last_lyric != lyric:
|
||
self.last_lyric = lyric
|
||
if self.lyrics_browser.fontMetrics().horizontalAdvance(lyric) > self.lyrics_container.width():
|
||
self.scroll_lyrics(lyric)
|
||
else:
|
||
if self.animation is not None:
|
||
self.animation.stop()
|
||
self.animation = None
|
||
self.lyrics_browser.setText(lyric)
|
||
self.lyrics_browser.adjustSize()
|
||
self.lyrics_browser.move((self.lyrics_container.width() - self.lyrics_browser.width()) // 2, 0)
|
||
|
||
def scroll_lyrics(self, lyric):
|
||
self.lyrics_browser.setText(lyric)
|
||
self.lyrics_browser.adjustSize()
|
||
label_width = self.lyrics_browser.width()
|
||
container_width = self.lyrics_container.width()
|
||
start_pos = QPoint(50, 0)
|
||
end_pos = QPoint(-label_width, 0)
|
||
if self.animation is not None:
|
||
self.animation.stop()
|
||
self.animation = QPropertyAnimation(self.lyrics_browser, b"pos")
|
||
duration = 8000 + (label_width - container_width) * 20
|
||
self.animation.setDuration(duration)
|
||
self.animation.setStartValue(start_pos)
|
||
self.animation.setEndValue(end_pos)
|
||
self.animation.start()
|
||
|
||
def update_lyrics_and_progress(self):
|
||
elapsed_time = time.time() - self.start_time
|
||
self.current_lyric_index = self.find_lyric_index(elapsed_time)
|
||
self.update_lyrics_display()
|
||
if self.music_length > 0:
|
||
progress = int((elapsed_time / self.music_length) * 100)
|
||
self.slider.setValue(progress)
|
||
|
||
if __name__ == '__main__':
|
||
app = QApplication(sys.argv)
|
||
player = MusicPlayer()
|
||
player.show()
|
||
sys.exit(app.exec())
|