import sys
import os
import random
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QSlider,
QFileDialog, QMessageBox, QStyle, QListWidget,
QListWidgetItem, QGroupBox, QMenu, QAction,
QComboBox, QProgressBar, QSplitter, QToolBar,
QStatusBar, QSystemTrayIcon)
from PyQt5.QtCore import Qt, QTimer, QTime, pyqtSignal, QSize, QEvent
from PyQt5.QtGui import QIcon, QFont, QPalette, QColor, QPixmap, QCursor
from player import AudioLibrary
import time
class ModernAudioPlayer(QMainWindow):
# 自定义信号
position_changed = pyqtSignal(float)
state_changed = pyqtSignal(str)
volume_changed = pyqtSignal(int)
# 播放模式常量
PLAY_MODE_SEQUENTIAL = 0 # 顺序播放
PLAY_MODE_SHUFFLE = 1 # 随机播放
PLAY_MODE_SINGLE = 2 # 单曲循环
PLAY_MODE_LOOP = 3 # 列表循环
def __init__(self):
super().__init__()
# 初始化音频库
try:
self.audio_lib = AudioLibrary()
print("音频库初始化成功")
except Exception as e:
QMessageBox.critical(self, "错误", f"音频库初始化失败: {e}")
self.audio_lib = None
return
# 初始化变量
self.current_aid = None
self.current_file = None
self.current_file_name = ""
self.playlist = []
self.current_index = -1
self.is_playing = False
self.is_paused = False
self.total_duration = 0
self.playback_start_time = 0
self.current_position = 0
self.volume = 80
self.is_muted = False
self.last_volume = 80
# 播放模式相关
self.play_mode = self.PLAY_MODE_SEQUENTIAL
self.shuffled_indices = [] # 随机播放时的播放顺序
self.is_shuffle_active = False
# 更新定时器
self.update_timer = QTimer()
# 初始化UI
self.init_ui()
# 连接信号和槽
self.setup_connections()
# 启动更新定时器
self.update_timer.start(100) # 100ms更新一次
# 初始化系统托盘
self.init_system_tray()
def init_ui(self):
"""初始化UI"""
self.setWindowTitle("Modern Audio Player - Advanced")
self.setGeometry(100, 100, 1000, 700)
# 设置应用程序图标
self.setWindowIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
# 设置深色主题
self.set_dark_theme()
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(10)
# 1. 顶部工具栏
self.create_toolbar()
# 2. 使用分割器创建左右布局
splitter = QSplitter(Qt.Horizontal)
# 左侧:播放控制和信息面板
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
left_layout.setContentsMargins(0, 0, 0, 0)
# 当前播放信息
self.create_current_info_panel(left_layout)
# 播放进度控制
self.create_progress_panel(left_layout)
# 播放控制按钮
self.create_control_panel(left_layout)
# 音量控制
self.create_volume_panel(left_layout)
# 播放模式选择
self.create_playmode_panel(left_layout)
# 右侧:播放列表
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(5)
# 播放列表标题和操作按钮
playlist_header = QWidget()
playlist_header_layout = QHBoxLayout(playlist_header)
playlist_header_layout.setContentsMargins(0, 0, 0, 0)
playlist_label = QLabel("🎵 播放列表")
playlist_label.setFont(QFont("Arial", 12, QFont.Bold))
playlist_label.setStyleSheet("color: #ecf0f1; padding: 5px;")
# 播放列表操作按钮
playlist_actions = QWidget()
playlist_actions_layout = QHBoxLayout(playlist_actions)
playlist_actions_layout.setContentsMargins(0, 0, 0, 0)
self.playlist_clear_button = QPushButton("清空")
self.playlist_remove_button = QPushButton("移除")
self.playlist_save_button = QPushButton("保存")
self.playlist_load_button = QPushButton("加载")
for btn in [self.playlist_clear_button, self.playlist_remove_button,
self.playlist_save_button, self.playlist_load_button]:
btn.setFixedSize(60, 25)
btn.setFont(QFont("Arial", 8))
playlist_actions_layout.addWidget(self.playlist_clear_button)
playlist_actions_layout.addWidget(self.playlist_remove_button)
playlist_actions_layout.addWidget(self.playlist_save_button)
playlist_actions_layout.addWidget(self.playlist_load_button)
playlist_actions_layout.addStretch()
playlist_header_layout.addWidget(playlist_label)
playlist_header_layout.addWidget(playlist_actions)
right_layout.addWidget(playlist_header)
# 播放列表
self.playlist_widget = QListWidget()
self.playlist_widget.setStyleSheet("""
QListWidget {
background-color: #2c3e50;
border: 2px solid #34495e;
border-radius: 8px;
padding: 5px;
color: #ecf0f1;
font-size: 11px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #34495e;
border-radius: 4px;
margin: 2px;
}
QListWidget::item:selected {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #3498db, stop: 1 #2980b9
);
color: white;
border: 1px solid #2980b9;
}
QListWidget::item:hover {
background-color: #34495e;
border: 1px solid #7f8c8d;
}
QListWidget::item:alternate {
background-color: #2d3e50;
}
""")
self.playlist_widget.setAlternatingRowColors(True)
right_layout.addWidget(self.playlist_widget)
# 播放列表信息
self.playlist_info_label = QLabel("共 0 首歌曲 | 总时长: 00:00")
self.playlist_info_label.setFont(QFont("Arial", 9))
self.playlist_info_label.setStyleSheet("color: #7f8c8d; padding: 5px;")
self.playlist_info_label.setAlignment(Qt.AlignCenter)
right_layout.addWidget(self.playlist_info_label)
# 设置分割器
splitter.addWidget(left_widget)
splitter.addWidget(right_widget)
splitter.setSizes([400, 600])
main_layout.addWidget(splitter)
# 3. 底部状态栏
self.create_status_bar()
# 设置按钮样式
self.set_button_styles()
# 初始化菜单
self.create_context_menus()
def create_toolbar(self):
"""创建工具栏"""
toolbar = QToolBar()
toolbar.setIconSize(QSize(24, 24))
toolbar.setMovable(False)
self.addToolBar(toolbar)
# 文件操作
self.open_file_action = QAction(
self.style().standardIcon(QStyle.SP_DirOpenIcon),
"打开文件", self
)
self.open_folder_action = QAction(
self.style().standardIcon(QStyle.SP_DirIcon),
"打开文件夹", self
)
toolbar.addAction(self.open_file_action)
toolbar.addAction(self.open_folder_action)
toolbar.addSeparator()
# 播放控制
self.play_action = QAction(
self.style().standardIcon(QStyle.SP_MediaPlay),
"播放", self
)
self.pause_action = QAction(
self.style().standardIcon(QStyle.SP_MediaPause),
"暂停", self
)
self.stop_action = QAction(
self.style().standardIcon(QStyle.SP_MediaStop),
"停止", self
)
self.prev_action = QAction(
self.style().standardIcon(QStyle.SP_MediaSkipBackward),
"上一首", self
)
self.next_action = QAction(
self.style().standardIcon(QStyle.SP_MediaSkipForward),
"下一首", self
)
toolbar.addAction(self.play_action)
toolbar.addAction(self.pause_action)
toolbar.addAction(self.stop_action)
toolbar.addAction(self.prev_action)
toolbar.addAction(self.next_action)
toolbar.addSeparator()
# 播放模式
self.playmode_combo = QComboBox()
self.playmode_combo.addItem("顺序播放", self.PLAY_MODE_SEQUENTIAL)
self.playmode_combo.addItem("随机播放", self.PLAY_MODE_SHUFFLE)
self.playmode_combo.addItem("单曲循环", self.PLAY_MODE_SINGLE)
self.playmode_combo.addItem("列表循环", self.PLAY_MODE_LOOP)
self.playmode_combo.setFixedWidth(120)
toolbar.addWidget(self.playmode_combo)
def create_current_info_panel(self, layout):
"""创建当前播放信息面板"""
info_group = QGroupBox("当前播放")
info_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #3498db;
border-radius: 10px;
margin-top: 5px;
padding-top: 15px;
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #2c3e50, stop: 1 #34495e
);
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #3498db;
font-size: 12px;
}
""")
info_layout = QVBoxLayout()
info_layout.setSpacing(8)
# 专辑封面(占位符)
album_frame = QWidget()
album_frame.setFixedHeight(150)
album_frame.setStyleSheet("""
background-color: #2c3e50;
border: 2px dashed #3498db;
border-radius: 8px;
""")
album_layout = QVBoxLayout(album_frame)
album_layout.setAlignment(Qt.AlignCenter)
album_icon = QLabel("🎵")
album_icon.setFont(QFont("Arial", 48))
album_icon.setAlignment(Qt.AlignCenter)
album_layout.addWidget(album_icon)
info_layout.addWidget(album_frame)
# 歌曲信息
self.current_file_label = QLabel("未选择歌曲")
self.current_file_label.setFont(QFont("Arial", 11, QFont.Bold))
self.current_file_label.setStyleSheet("color: #ecf0f1; padding: 5px;")
self.current_file_label.setAlignment(Qt.AlignCenter)
self.current_file_label.setWordWrap(True)
self.song_info_label = QLabel("艺术家: 未知 | 专辑: 未知")
self.song_info_label.setFont(QFont("Arial", 9))
self.song_info_label.setStyleSheet("color: #bdc3c7; padding: 3px;")
self.song_info_label.setAlignment(Qt.AlignCenter)
info_layout.addWidget(self.current_file_label)
info_layout.addWidget(self.song_info_label)
info_group.setLayout(info_layout)
layout.addWidget(info_group)
def create_progress_panel(self, layout):
"""创建进度控制面板"""
progress_group = QGroupBox("播放进度")
progress_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #2ecc71;
border-radius: 10px;
margin-top: 5px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #2ecc71;
font-size: 12px;
}
""")
progress_layout = QVBoxLayout()
progress_layout.setSpacing(8)
# 时间显示
time_layout = QHBoxLayout()
self.current_time_label = QLabel("00:00")
self.current_time_label.setFont(QFont("Arial", 11))
self.current_time_label.setStyleSheet("color: #ecf0f1;")
self.total_time_label = QLabel("00:00")
self.total_time_label.setFont(QFont("Arial", 11))
self.total_time_label.setStyleSheet("color: #ecf0f1;")
self.total_time_label.setAlignment(Qt.AlignRight)
time_layout.addWidget(self.current_time_label)
time_layout.addStretch()
time_layout.addWidget(self.total_time_label)
progress_layout.addLayout(time_layout)
# 进度条
self.position_slider = QSlider(Qt.Horizontal)
self.position_slider.setRange(0, 1000)
self.position_slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #27ae60;
background: #2c3e50;
height: 8px;
border-radius: 4px;
}
QSlider::sub-page:horizontal {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #2ecc71, stop: 1 #27ae60
);
border: 1px solid #27ae60;
height: 8px;
border-radius: 4px;
}
QSlider::add-page:horizontal {
background: #34495e;
border: 1px solid #27ae60;
height: 8px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 #ecf0f1, stop: 1 #bdc3c7
);
width: 18px;
height: 18px;
margin: -5px 0;
border-radius: 9px;
border: 1px solid #7f8c8d;
}
QSlider::handle:horizontal:hover {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 #ffffff, stop: 1 #ecf0f1
);
border: 2px solid #2ecc71;
}
""")
progress_layout.addWidget(self.position_slider)
progress_group.setLayout(progress_layout)
layout.addWidget(progress_group)
def create_control_panel(self, layout):
"""创建控制按钮面板"""
control_group = QGroupBox("播放控制")
control_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #e74c3c;
border-radius: 10px;
margin-top: 5px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #e74c3c;
font-size: 12px;
}
""")
control_layout = QHBoxLayout()
control_layout.setSpacing(15)
# 创建控制按钮
buttons = [
("⏮", "上一首", self.play_previous, 50, 50),
("⏪", "快退10秒", lambda: self.seek_relative(-10), 45, 45),
("▶", "播放", self.play_pause, 60, 60),
("⏩", "快进10秒", lambda: self.seek_relative(10), 45, 45),
("⏭", "下一首", self.play_next, 50, 50),
("⏹", "停止", self.stop, 45, 45),
]
for icon, tooltip, slot, w, h in buttons:
btn = QPushButton(icon)
btn.setFont(QFont("Arial", 14))
btn.setFixedSize(w, h)
btn.setToolTip(tooltip)
btn.clicked.connect(slot)
# 为播放按钮特殊处理
if icon == "▶":
self.play_button = btn
elif icon == "⏹":
self.stop_button = btn
control_layout.addWidget(btn)
control_layout.insertStretch(0, 1)
control_layout.addStretch(1)
control_group.setLayout(control_layout)
layout.addWidget(control_group)
def create_volume_panel(self, layout):
"""创建音量控制面板"""
volume_group = QGroupBox("音量控制")
volume_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #9b59b6;
border-radius: 10px;
margin-top: 5px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #9b59b6;
font-size: 12px;
}
""")
volume_layout = QHBoxLayout()
volume_layout.setSpacing(10)
# 静音按钮
self.mute_button = QPushButton("🔊")
self.mute_button.setFont(QFont("Arial", 12))
self.mute_button.setFixedSize(40, 40)
self.mute_button.setToolTip("静音")
# 音量滑块
self.volume_slider = QSlider(Qt.Horizontal)
self.volume_slider.setRange(0, 128)
self.volume_slider.setValue(80)
self.volume_slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #8e44ad;
background: #2c3e50;
height: 8px;
border-radius: 4px;
}
QSlider::sub-page:horizontal {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #9b59b6, stop: 1 #8e44ad
);
border: 1px solid #8e44ad;
height: 8px;
border-radius: 4px;
}
QSlider::add-page:horizontal {
background: #34495e;
border: 1px solid #8e44ad;
height: 8px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 #ecf0f1, stop: 1 #bdc3c7
);
width: 18px;
height: 18px;
margin: -5px 0;
border-radius: 9px;
border: 1px solid #7f8c8d;
}
QSlider::handle:horizontal:hover {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 #ffffff, stop: 1 #ecf0f1
);
border: 2px solid #9b59b6;
}
""")
# 音量标签
self.volume_label = QLabel("80")
self.volume_label.setFont(QFont("Arial", 11))
self.volume_label.setFixedWidth(30)
self.volume_label.setStyleSheet("color: #ecf0f1;")
volume_layout.addWidget(self.mute_button)
volume_layout.addWidget(self.volume_slider)
volume_layout.addWidget(self.volume_label)
volume_group.setLayout(volume_layout)
layout.addWidget(volume_group)
def create_playmode_panel(self, layout):
"""创建播放模式面板"""
playmode_group = QGroupBox("播放模式")
playmode_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #f39c12;
border-radius: 10px;
margin-top: 5px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 10px 0 10px;
color: #f39c12;
font-size: 12px;
}
""")
playmode_layout = QVBoxLayout()
playmode_layout.setSpacing(8)
# 播放模式描述
self.playmode_desc_label = QLabel("顺序播放: 按列表顺序播放,播放完停止")
self.playmode_desc_label.setFont(QFont("Arial", 9))
self.playmode_desc_label.setStyleSheet("color: #bdc3c7;")
self.playmode_desc_label.setWordWrap(True)
# 模式按钮组
mode_buttons_layout = QHBoxLayout()
self.mode_sequential = QPushButton("顺序")
self.mode_shuffle = QPushButton("随机")
self.mode_single = QPushButton("单曲")
self.mode_loop = QPushButton("列表")
for btn in [self.mode_sequential, self.mode_shuffle,
self.mode_single, self.mode_loop]:
btn.setFixedSize(60, 30)
btn.setCheckable(True)
# 设置顺序播放为默认选中
self.mode_sequential.setChecked(True)
mode_buttons_layout.addWidget(self.mode_sequential)
mode_buttons_layout.addWidget(self.mode_shuffle)
mode_buttons_layout.addWidget(self.mode_single)
mode_buttons_layout.addWidget(self.mode_loop)
mode_buttons_layout.addStretch()
playmode_layout.addWidget(self.playmode_desc_label)
playmode_layout.addLayout(mode_buttons_layout)
playmode_group.setLayout(playmode_layout)
layout.addWidget(playmode_group)
# 添加弹簧使布局更紧凑
layout.addStretch()
def create_status_bar(self):
"""创建状态栏"""
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# 状态标签
self.status_label = QLabel("就绪")
self.status_label.setFont(QFont("Arial", 9))
self.status_label.setStyleSheet("color: #7f8c8d;")
# 播放状态
self.play_status_label = QLabel("已停止")
self.play_status_label.setFont(QFont("Arial", 9))
self.play_status_label.setStyleSheet("color: #3498db;")
# 时间显示
self.time_status_label = QLabel("00:00 / 00:00")
self.time_status_label.setFont(QFont("Arial", 9))
self.time_status_label.setStyleSheet("color: #2ecc71;")
self.status_bar.addWidget(self.status_label, 1)
self.status_bar.addPermanentWidget(self.play_status_label)
self.status_bar.addPermanentWidget(self.time_status_label)
def create_context_menus(self):
"""创建上下文菜单"""
# 播放列表右键菜单
self.playlist_widget.setContextMenuPolicy(Qt.CustomContextMenu)
self.playlist_widget.customContextMenuRequested.connect(self.show_playlist_context_menu)
def set_dark_theme(self):
"""设置深色主题"""
palette = QPalette()
palette.setColor(QPalette.Window, QColor(40, 44, 52))
palette.setColor(QPalette.WindowText, QColor(220, 220, 220))
palette.setColor(QPalette.Base, QColor(30, 33, 40))
palette.setColor(QPalette.AlternateBase, QColor(35, 38, 45))
palette.setColor(QPalette.ToolTipBase, QColor(220, 220, 220))
palette.setColor(QPalette.ToolTipText, QColor(220, 220, 220))
palette.setColor(QPalette.Text, QColor(220, 220, 220))
palette.setColor(QPalette.Button, QColor(50, 54, 63))
palette.setColor(QPalette.ButtonText, QColor(220, 220, 220))
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Link, QColor(66, 139, 202))
palette.setColor(QPalette.Highlight, QColor(66, 139, 202))
palette.setColor(QPalette.HighlightedText, Qt.black)
self.setPalette(palette)
def set_button_styles(self):
"""设置按钮样式"""
# 控制按钮样式
control_button_style = """
QPushButton {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #34495e, stop: 1 #2c3e50
);
color: #ecf0f1;
border: 2px solid #3498db;
border-radius: 25px;
font-weight: bold;
}
QPushButton:hover {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #3498db, stop: 1 #2980b9
);
border: 2px solid #2980b9;
color: white;
}
QPushButton:pressed {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #2980b9, stop: 1 #3498db
);
}
QPushButton:checked {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #2ecc71, stop: 1 #27ae60
);
border: 2px solid #27ae60;
color: white;
}
"""
# 播放按钮特殊样式
play_button_style = """
QPushButton {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #2ecc71, stop: 1 #27ae60
);
color: white;
border: 2px solid #27ae60;
border-radius: 30px;
font-weight: bold;
}
QPushButton:hover {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #27ae60, stop: 1 #2ecc71
);
border: 2px solid #2ecc71;
}
QPushButton:pressed {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #229954, stop: 1 #27ae60
);
}
"""
# 设置播放按钮样式
self.play_button.setStyleSheet(play_button_style)
# 设置其他控制按钮样式
for btn in [self.stop_button, self.mute_button]:
btn.setStyleSheet(control_button_style)
# 模式按钮样式
mode_button_style = """
QPushButton {
background-color: #34495e;
color: #ecf0f1;
border: 1px solid #f39c12;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #f39c12;
color: white;
}
QPushButton:checked {
background-color: #e67e22;
color: white;
border: 2px solid #d35400;
}
"""
for btn in [self.mode_sequential, self.mode_shuffle,
self.mode_single, self.mode_loop]:
btn.setStyleSheet(mode_button_style)
# 播放列表按钮样式
playlist_button_style = """
QPushButton {
background-color: #2c3e50;
color: #ecf0f1;
border: 1px solid #3498db;
border-radius: 3px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3498db;
color: white;
}
"""
for btn in [self.playlist_clear_button, self.playlist_remove_button,
self.playlist_save_button, self.playlist_load_button]:
btn.setStyleSheet(playlist_button_style)
def setup_connections(self):
"""连接信号和槽"""
# 工具栏动作连接
self.open_file_action.triggered.connect(self.open_file)
self.open_folder_action.triggered.connect(self.open_folder)
self.play_action.triggered.connect(self.play_pause)
self.pause_action.triggered.connect(self.pause)
self.stop_action.triggered.connect(self.stop)
self.prev_action.triggered.connect(self.play_previous)
self.next_action.triggered.connect(self.play_next)
# 播放模式按钮连接
self.mode_sequential.clicked.connect(lambda: self.set_play_mode(self.PLAY_MODE_SEQUENTIAL))
self.mode_shuffle.clicked.connect(lambda: self.set_play_mode(self.PLAY_MODE_SHUFFLE))
self.mode_single.clicked.connect(lambda: self.set_play_mode(self.PLAY_MODE_SINGLE))
self.mode_loop.clicked.connect(lambda: self.set_play_mode(self.PLAY_MODE_LOOP))
# 播放模式组合框连接
self.playmode_combo.currentIndexChanged.connect(self.on_playmode_changed)
# 播放列表按钮连接
self.playlist_clear_button.clicked.connect(self.clear_playlist)
self.playlist_remove_button.clicked.connect(self.remove_selected_items)
self.playlist_save_button.clicked.connect(self.save_playlist)
self.playlist_load_button.clicked.connect(self.load_playlist)
# 滑块连接
self.position_slider.sliderMoved.connect(self.seek_position)
self.volume_slider.valueChanged.connect(self.set_volume)
# 播放列表连接
self.playlist_widget.itemDoubleClicked.connect(self.playlist_item_double_clicked)
# 静音按钮
self.mute_button.clicked.connect(self.toggle_mute)
# 定时器连接
self.update_timer.timeout.connect(self.update_ui)
def init_system_tray(self):
"""初始化系统托盘"""
if QSystemTrayIcon.isSystemTrayAvailable():
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
# 创建托盘菜单
tray_menu = QMenu()
play_action = QAction("播放/暂停", self)
play_action.triggered.connect(self.play_pause)
stop_action = QAction("停止", self)
stop_action.triggered.connect(self.stop)
next_action = QAction("下一首", self)
next_action.triggered.connect(self.play_next)
show_action = QAction("显示窗口", self)
show_action.triggered.connect(self.show_normal)
quit_action = QAction("退出", self)
quit_action.triggered.connect(self.quit_application)
tray_menu.addAction(play_action)
tray_menu.addAction(stop_action)
tray_menu.addAction(next_action)
tray_menu.addSeparator()
tray_menu.addAction(show_action)
tray_menu.addSeparator()
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.show()
# 托盘图标点击事件
self.tray_icon.activated.connect(self.on_tray_icon_activated)
def show_playlist_context_menu(self, position):
"""显示播放列表右键菜单"""
menu = QMenu()
play_action = QAction("播放", self)
play_action.triggered.connect(lambda: self.play_selected_item())
remove_action = QAction("移除", self)
remove_action.triggered.connect(self.remove_selected_items)
clear_action = QAction("清空列表", self)
clear_action.triggered.connect(self.clear_playlist)
menu.addAction(play_action)
menu.addAction(remove_action)
menu.addSeparator()
menu.addAction(clear_action)
menu.exec_(self.playlist_widget.mapToGlobal(position))
def open_file(self):
"""打开单个文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择音频文件", "",
"音频文件 (*.mp3 *.wav *.ogg *.flac *.m4a *.aac);;所有文件 (*.*)"
)
if file_path:
self.add_to_playlist(file_path)
if len(self.playlist) == 1: # 如果是第一个文件,自动加载
self.load_file(file_path)
def open_folder(self):
"""打开文件夹并添加所有音频文件"""
folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹")
if folder_path:
# 清空当前播放列表
self.playlist.clear()
self.playlist_widget.clear()
self.shuffled_indices.clear()
# 遍历文件夹中的音频文件
audio_extensions = ['.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac']
for root, dirs, files in os.walk(folder_path):
for file in files:
if any(file.lower().endswith(ext) for ext in audio_extensions):
file_path = os.path.join(root, file)
self.add_to_playlist(file_path)
if self.playlist:
self.status_label.setText(f"已添加 {len(self.playlist)} 个文件到播放列表")
self.update_playlist_info()
# 如果当前没有在播放,自动加载第一个文件
if not self.is_playing:
self.load_file(self.playlist[0])
def add_to_playlist(self, file_path):
"""添加文件到播放列表"""
if file_path not in self.playlist:
self.playlist.append(file_path)
file_name = os.path.basename(file_path)
# 获取文件信息
try:
duration_result = self.audio_lib.get_audio_duration(file_path, is_file=True)
if isinstance(duration_result, tuple):
duration = 0
else:
duration = int(duration_result) if duration_result else 0
duration_str = self.format_time(duration)
display_text = f"{file_name} ({duration_str})"
except:
display_text = file_name
item = QListWidgetItem(f"🎵 {display_text}")
item.setData(Qt.UserRole, file_path)
item.setToolTip(file_path)
self.playlist_widget.addItem(item)
# 设置高亮当前播放的项目
if self.current_file == file_path:
item.setSelected(True)
self.playlist_widget.scrollToItem(item)
# 更新播放列表信息
self.update_playlist_info()
def update_playlist_info(self):
"""更新播放列表信息"""
count = len(self.playlist)
# 计算总时长
total_seconds = 0
for file_path in self.playlist:
try:
duration_result = self.audio_lib.get_audio_duration(file_path, is_file=True)
if isinstance(duration_result, tuple):
continue
total_seconds += int(duration_result) if duration_result else 0
except:
continue
total_time = self.format_time(total_seconds)
self.playlist_info_label.setText(f"共 {count} 首歌曲 | 总时长: {total_time}")
def remove_selected_items(self):
"""移除选中的播放列表项"""
selected_items = self.playlist_widget.selectedItems()
if not selected_items:
return
reply = QMessageBox.question(
self, "确认", f"确定要移除选中的 {len(selected_items)} 个项目吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
for item in selected_items:
file_path = item.data(Qt.UserRole)
if file_path in self.playlist:
index = self.playlist.index(file_path)
self.playlist.pop(index)
# 如果移除的是当前播放的文件
if file_path == self.current_file:
self.stop()
self.current_file = None
self.current_index = -1
self.playlist_widget.takeItem(self.playlist_widget.row(item))
self.update_playlist_info()
def save_playlist(self):
"""保存播放列表到文件"""
if not self.playlist:
QMessageBox.warning(self, "警告", "播放列表为空,无法保存")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存播放列表", "", "播放列表文件 (*.m3u);;文本文件 (*.txt)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
for item_path in self.playlist:
f.write(item_path + '\n')
self.status_label.setText(f"播放列表已保存到: {file_path}")
QMessageBox.information(self, "成功", "播放列表保存成功!")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存播放列表失败: {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 = f.readlines()
# 清空当前播放列表
self.playlist.clear()
self.playlist_widget.clear()
self.shuffled_indices.clear()
for line in lines:
line = line.strip()
if line and os.path.exists(line):
self.add_to_playlist(line)
if self.playlist:
self.status_label.setText(f"已加载 {len(self.playlist)} 个文件")
QMessageBox.information(self, "成功", "播放列表加载成功!")
else:
QMessageBox.warning(self, "警告", "播放列表文件为空或文件路径无效")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载播放列表失败: {e}")
def clear_playlist(self):
"""清空播放列表"""
if not self.playlist:
return
reply = QMessageBox.question(
self, "确认", "确定要清空播放列表吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.playlist.clear()
self.playlist_widget.clear()
self.shuffled_indices.clear()
self.current_index = -1
self.current_file = None
self.current_file_label.setText("未选择歌曲")
self.song_info_label.setText("艺术家: 未知 | 专辑: 未知")
self.update_playlist_info()
# 停止当前播放
if self.is_playing and self.current_aid:
self.stop()
def load_file(self, file_path):
"""加载音频文件"""
try:
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
# 停止当前播放
if self.is_playing and self.current_aid:
try:
self.audio_lib.stop_audio(self.current_aid)
except:
pass
self.is_playing = False
self.is_paused = False
self.play_button.setText("▶")
# 设置当前文件
self.current_file = file_path
self.current_file_name = os.path.basename(file_path)
# 更新显示信息
self.current_file_label.setText(self.current_file_name)
# 尝试提取艺术家和专辑信息
file_dir = os.path.dirname(file_path)
folder_name = os.path.basename(file_dir)
self.song_info_label.setText(f"艺术家: 未知 | 专辑: {folder_name}")
# 更新当前索引
if file_path in self.playlist:
self.current_index = self.playlist.index(file_path)
# 如果是随机播放模式,更新随机索引
if self.play_mode == self.PLAY_MODE_SHUFFLE:
if not self.shuffled_indices:
self.generate_shuffle_indices()
# 高亮当前播放的项目
for i in range(self.playlist_widget.count()):
item = self.playlist_widget.item(i)
if item.data(Qt.UserRole) == file_path:
item.setSelected(True)
self.playlist_widget.scrollToItem(item)
# 更新项目文本,添加播放标志
current_text = item.text()
if not current_text.startswith("▶ "):
item.setText("▶ " + current_text.lstrip("🎵 ▶ "))
else:
item.setSelected(False)
# 移除其他项目的播放标志
text = item.text()
if text.startswith("▶ "):
item.setText("🎵 " + text[2:])
# 获取音频时长
try:
duration_result = self.audio_lib.get_audio_duration(file_path, is_file=True)
if isinstance(duration_result, tuple):
self.total_duration = 300 # 默认5分钟
else:
self.total_duration = float(duration_result) if duration_result else 300
except:
self.total_duration = 300
# 更新总时长显示
self.total_time_label.setText(self.format_time(self.total_duration))
# 加载音频到内存
try:
if self.current_aid:
try:
old_file = self.audio_lib._get_file_path_by_aid(self.current_aid)
if old_file in self.audio_lib._audio_cache:
self.audio_lib._audio_cache.pop(old_file, None)
elif old_file in self.audio_lib._music_cache:
self.audio_lib._music_cache.pop(old_file, None)
except:
pass
self.current_aid = self.audio_lib.new_aid(file_path)
print(f"加载音频成功,AID: {self.current_aid}")
# 设置音量和播放速度
self.audio_lib.set_volume(self.current_aid, self.volume)
self.status_label.setText(f"已加载: {self.current_file_name}")
self.play_status_label.setText("已加载")
except Exception as e:
raise Exception(f"加载音频失败: {e}")
except Exception as e:
QMessageBox.warning(self, "警告", f"加载文件失败: {e}")
self.status_label.setText(f"加载失败: {e}")
def play_selected_item(self):
"""播放选中的项目"""
selected_items = self.playlist_widget.selectedItems()
if selected_items:
item = selected_items[0]
file_path = item.data(Qt.UserRole)
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
def playlist_item_double_clicked(self, item):
"""播放列表项双击事件"""
file_path = item.data(Qt.UserRole)
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
def play_pause(self):
"""播放/暂停"""
if not self.current_file or not self.current_aid:
if self.playlist:
# 如果没有当前文件,但有播放列表,播放第一个
self.load_file(self.playlist[0])
if self.current_aid:
self.start_playback()
else:
self.status_label.setText("请先选择音频文件")
return
try:
if not self.is_playing:
self.start_playback()
else:
if self.is_paused:
self.resume_playback()
else:
self.pause_playback()
except Exception as e:
print(f"播放控制失败: {e}")
self.status_label.setText(f"播放失败: {e}")
def start_playback(self):
"""开始播放"""
print(f"开始播放,AID: {self.current_aid}")
try:
self.current_aid = self.audio_lib.play_from_memory(self.current_file)
except Exception as e:
print(f"从内存播放失败,尝试从文件播放: {e}")
self.current_aid = self.audio_lib.play_from_file(self.current_file)
self.is_playing = True
self.is_paused = False
self.play_button.setText("⏸")
self.playback_start_time = time.time()
self.current_position = 0
# 设置音量和播放速度
self.audio_lib.set_volume(self.current_aid, self.volume)
self.status_label.setText(f"正在播放: {self.current_file_name}")
self.play_status_label.setText("播放中")
self.state_changed.emit("playing")
def pause_playback(self):
"""暂停播放"""
print("暂停播放")
self.audio_lib.pause_audio(self.current_aid)
self.is_paused = True
self.play_button.setText("▶")
self.current_position = time.time() - self.playback_start_time
self.status_label.setText(f"已暂停: {self.current_file_name}")
self.play_status_label.setText("已暂停")
self.state_changed.emit("paused")
def resume_playback(self):
"""恢复播放"""
print("恢复播放")
self.audio_lib.play_audio(self.current_aid)
self.is_paused = False
self.play_button.setText("⏸")
self.playback_start_time = time.time() - self.current_position
self.status_label.setText(f"正在播放: {self.current_file_name}")
self.play_status_label.setText("播放中")
self.state_changed.emit("playing")
def pause(self):
"""暂停播放(工具栏专用)"""
if self.is_playing and not self.is_paused:
self.pause_playback()
def stop(self):
"""停止播放"""
if self.is_playing and self.current_aid:
try:
print("停止播放")
played_time = self.audio_lib.stop_audio(self.current_aid)
print(f"已停止播放,播放时长: {played_time:.2f}秒")
self.is_playing = False
self.is_paused = False
self.play_button.setText("▶")
self.position_slider.setValue(0)
self.current_position = 0
self.current_time_label.setText("00:00")
self.status_label.setText(f"已停止: {self.current_file_name}")
self.play_status_label.setText("已停止")
self.state_changed.emit("stopped")
except Exception as e:
print(f"停止播放失败: {e}")
self.status_label.setText(f"停止失败: {e}")
def play_previous(self):
"""播放上一首"""
if not self.playlist:
return
if self.current_index >= 0:
if self.play_mode == self.PLAY_MODE_SHUFFLE:
self.play_previous_shuffle()
else:
if self.current_index > 0:
self.current_index -= 1
elif self.play_mode == self.PLAY_MODE_LOOP:
self.current_index = len(self.playlist) - 1
else:
return
file_path = self.playlist[self.current_index]
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
def play_previous_shuffle(self):
"""随机播放模式下的上一首"""
if not self.shuffled_indices:
self.generate_shuffle_indices()
current_shuffle_index = self.get_current_shuffle_index()
if current_shuffle_index > 0:
new_index = self.shuffled_indices[current_shuffle_index - 1]
self.current_index = new_index
file_path = self.playlist[self.current_index]
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
def play_next(self):
"""播放下一首"""
if not self.playlist:
return
if self.play_mode == self.PLAY_MODE_SINGLE:
# 单曲循环,重新播放当前歌曲
if self.current_file:
self.load_file(self.current_file)
if not self.is_playing:
self.play_pause()
return
if self.current_index >= 0:
if self.play_mode == self.PLAY_MODE_SHUFFLE:
self.play_next_shuffle()
else:
if self.current_index < len(self.playlist) - 1:
self.current_index += 1
elif self.play_mode == self.PLAY_MODE_LOOP:
self.current_index = 0
else:
# 顺序播放到最后一首,停止播放
self.stop()
return
file_path = self.playlist[self.current_index]
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
def play_next_shuffle(self):
"""随机播放模式下的下一首"""
if not self.shuffled_indices:
self.generate_shuffle_indices()
current_shuffle_index = self.get_current_shuffle_index()
if current_shuffle_index < len(self.shuffled_indices) - 1:
new_index = self.shuffled_indices[current_shuffle_index + 1]
self.current_index = new_index
file_path = self.playlist[self.current_index]
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
else:
# 随机播放列表结束,重新生成或停止
if self.play_mode == self.PLAY_MODE_SHUFFLE:
self.generate_shuffle_indices()
if self.shuffled_indices:
self.current_index = self.shuffled_indices[0]
file_path = self.playlist[self.current_index]
self.load_file(file_path)
if not self.is_playing:
self.play_pause()
def generate_shuffle_indices(self):
"""生成随机播放的索引列表"""
if not self.playlist:
return
indices = list(range(len(self.playlist)))
random.shuffle(indices)
# 确保当前播放的歌曲不在第一个位置(如果可能)
if self.current_index in indices and indices[0] == self.current_index and len(indices) > 1:
indices[0], indices[1] = indices[1], indices[0]
self.shuffled_indices = indices
def get_current_shuffle_index(self):
"""获取当前歌曲在随机播放列表中的位置"""
if self.current_index in self.shuffled_indices:
return self.shuffled_indices.index(self.current_index)
return -1
def set_play_mode(self, mode):
"""设置播放模式"""
self.play_mode = mode
# 更新按钮状态
self.mode_sequential.setChecked(mode == self.PLAY_MODE_SEQUENTIAL)
self.mode_shuffle.setChecked(mode == self.PLAY_MODE_SHUFFLE)
self.mode_single.setChecked(mode == self.PLAY_MODE_SINGLE)
self.mode_loop.setChecked(mode == self.PLAY_MODE_LOOP)
# 更新组合框
self.playmode_combo.setCurrentIndex(mode)
# 更新模式描述
descriptions = [
"顺序播放: 按列表顺序播放,播放完停止",
"随机播放: 随机播放列表中的歌曲",
"单曲循环: 重复播放当前歌曲",
"列表循环: 按列表顺序循环播放"
]
self.playmode_desc_label.setText(descriptions[mode])
# 如果是随机播放模式,生成随机索引
if mode == self.PLAY_MODE_SHUFFLE:
self.generate_shuffle_indices()
self.status_label.setText(f"播放模式已切换: {self.playmode_combo.currentText()}")
def on_playmode_changed(self, index):
"""播放模式组合框变化"""
mode = self.playmode_combo.currentData()
self.set_play_mode(mode)
def toggle_mute(self):
"""切换静音"""
if not self.is_muted:
self.last_volume = self.volume_slider.value()
self.volume_slider.setValue(0)
self.mute_button.setText("🔇")
self.is_muted = True
else:
self.volume_slider.setValue(self.last_volume)
self.mute_button.setText("🔊")
self.is_muted = False
def set_volume(self, value):
"""设置音量"""
self.volume = value
self.volume_label.setText(str(value))
if self.current_aid and self.is_playing:
try:
self.audio_lib.set_volume(self.current_aid, value)
except Exception as e:
print(f"设置音量失败: {e}")
# 更新静音状态
if value == 0:
self.is_muted = True
self.mute_button.setText("🔇")
else:
self.is_muted = False
self.mute_button.setText("🔊")
self.volume_changed.emit(value)
def on_speed_changed(self, index):
"""播放速度变化"""
pass
def seek_position(self, value):
"""跳转到指定位置"""
if self.total_duration > 0:
position = (value / 1000.0) * self.total_duration
if self.is_playing and self.current_aid:
try:
self.audio_lib.seek_audio(self.current_aid, position)
self.playback_start_time = time.time() - position
self.current_position = position
except Exception as e:
print(f"跳转失败: {e}")
def seek_relative(self, seconds):
"""相对跳转(快进/快退)"""
if self.is_playing and self.current_aid and self.total_duration > 0:
current_time = time.time() - self.playback_start_time
new_position = max(0, min(self.total_duration, current_time + seconds))
try:
self.audio_lib.seek_audio(self.current_aid, new_position)
self.playback_start_time = time.time() - new_position
self.current_position = new_position
except Exception as e:
print(f"跳转失败: {e}")
def update_ui(self):
"""更新UI"""
try:
if self.is_playing and not self.is_paused and self.current_aid:
if self.playback_start_time > 0:
current_time = time.time() - self.playback_start_time
self.current_position = current_time
# 防止超出总时长
if self.total_duration > 0 and current_time >= self.total_duration:
current_time = self.total_duration
# 歌曲播放结束,根据播放模式处理
self.on_track_end()
if not self.is_playing:
return
# 更新时间显示
self.current_time_label.setText(self.format_time(current_time))
self.time_status_label.setText(
f"{self.format_time(current_time)} / {self.format_time(self.total_duration)}"
)
# 更新进度条
if self.total_duration > 0:
progress_value = int((current_time / self.total_duration) * 1000)
self.position_slider.setValue(progress_value)
except Exception as e:
print(f"更新UI时出错: {e}")
def on_track_end(self):
"""处理歌曲播放结束"""
if self.play_mode == self.PLAY_MODE_SINGLE:
# 单曲循环,重新播放
self.load_file(self.current_file)
self.start_playback()
else:
# 其他模式,播放下一首
self.play_next()
def format_time(self, seconds):
"""格式化时间显示"""
if not seconds:
seconds = 0
minutes = int(seconds // 60)
secs = int(seconds % 60)
return f"{minutes:02d}:{secs:02d}"
def show_normal(self):
"""显示窗口"""
self.show()
self.activateWindow()
self.raise_()
def on_tray_icon_activated(self, reason):
"""托盘图标激活"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_normal()
def quit_application(self):
"""退出应用程序"""
self.cleanup()
QApplication.quit()
def closeEvent(self, event):
"""关闭窗口事件"""
if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
# 如果有托盘图标,最小化到托盘
self.hide()
event.ignore()
self.tray_icon.showMessage(
"Modern Audio Player",
"程序已最小化到系统托盘",
QSystemTrayIcon.Information,
2000
)
else:
# 否则正常退出
self.cleanup()
event.accept()
def cleanup(self):
"""清理资源"""
try:
if self.audio_lib:
print("清理音频库资源...")
if self.current_aid and self.is_playing:
try:
self.audio_lib.stop_audio(self.current_aid)
except:
pass
self.audio_lib.clear_memory_cache()
except Exception as e:
print(f"清理资源时出错: {e}")
def main():
app = QApplication(sys.argv)
app.setApplicationName("Modern Audio Player")
app.setApplicationDisplayName("Modern Audio Player")
# 设置应用程序样式
app.setStyle('Fusion')
# 设置应用程序图标
app.setWindowIcon(QIcon(":icons/media-play"))
player = ModernAudioPlayer()
player.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()