DreamLife_MusicPlayer/musicplayer.py
2025-03-22 19:38:02 +08:00

303 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())