import
sys
import
os
from
PyQt5.QtWidgets
import
(
QApplication, QWidget, QLabel, QPushButton, QSlider, QHBoxLayout, QVBoxLayout, QGridLayout, QFileDialog, QListWidget, QListWidgetItem, QMenu
)
from
PyQt5.QtCore
import
Qt, QTimer, QUrl, QByteArray, pyqtSignal
from
PyQt5.QtGui
import
QPixmap, QPainter, QTransform, QPainterPath, QFont, QColor, QLinearGradient, QBrush, QPen, QCursor
from
mutagen.mp3
import
MP3
from
mutagen.id3
import
ID3, APIC
from
PyQt5.QtMultimedia
import
QMediaPlayer, QMediaContent
import
vlc
import
re
def
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
=
0
self
.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):
base, _
=
os.path.splitext(song_path)
jpg_path
=
base
+
".jpg"
if
os.path.exists(jpg_path):
return
QPixmap(jpg_path)
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
pixmap
except
Exception as e:
pass
if
os.path.exists(default_cover):
return
QPixmap(default_cover)
return
QPixmap()
def
rotate(
self
):
if
self
.pixmap.isNull():
return
self
.angle
=
(
self
.angle
+
2
)
%
360
target_size
=
250
cover_scaled
=
self
.pixmap.scaled(target_size, target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
cover_rotated
=
cover_scaled.transformed(QTransform().rotate(
self
.angle), Qt.SmoothTransformation)
cover_circle
=
QPixmap(target_size, target_size)
cover_circle.fill(Qt.transparent)
painter
=
QPainter(cover_circle)
painter.setRenderHint(QPainter.Antialiasing)
path
=
QPainterPath()
path.addEllipse(
0
,
0
, target_size, target_size)
painter.setClipPath(path)
x
=
(target_size
-
cover_rotated.width())
/
/
2
y
=
(target_size
-
cover_rotated.height())
/
/
2
painter.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
=
0
self
.timer
=
QTimer(
self
)
self
.timer.timeout.connect(
self
.rotate)
self
.timer.start(
50
)
self
.rotate()
self
.default_cover
=
default_cover
def
rotate(
self
):
self
.angle
=
(
self
.angle
+
2
)
%
360
self
.update()
def
setCover(
self
, song_path):
pixmap
=
self
.load_cover(song_path,
self
.default_cover)
self
.cover_pixmap
=
pixmap
self
.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()
base, _
=
os.path.splitext(song_path)
jpg_path
=
base
+
".jpg"
if
os.path.exists(jpg_path):
return
QPixmap(jpg_path)
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
pixmap
except
Exception as e:
pass
if
os.path.exists(default_cover):
return
QPixmap(default_cover)
return
QPixmap()
def
paintEvent(
self
, event):
painter
=
QPainter(
self
)
painter.setRenderHint(QPainter.Antialiasing)
if
not
self
.bg_pixmap.isNull():
bg
=
self
.bg_pixmap.scaled(
250
,
250
, Qt.KeepAspectRatio, Qt.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()
if
not
self
.cover_pixmap.isNull():
size
=
130
src
=
self
.cover_pixmap
w, h
=
src.width(), src.height()
if
w !
=
h:
side
=
min
(w, h)
x
=
(w
-
side)
/
/
2
y
=
(h
-
side)
/
/
2
src
=
src.copy(x, y, side, side)
cover
=
src.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
cover_circle
=
QPixmap(size, size)
cover_circle.fill(Qt.transparent)
p
=
QPainter(cover_circle)
p.setRenderHint(QPainter.Antialiasing)
path
=
QPainterPath()
path.addEllipse(
0
,
0
, size, size)
p.setClipPath(path)
p.drawPixmap(
0
,
0
, cover)
p.end()
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
=
[]
self
.current_index
=
0
self
.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
=
0
self
.demo_text
=
text
self
.update()
def
setLyrics(
self
, lyrics):
self
.lyrics
=
lyrics
self
.current_index
=
0
self
.demo_text
=
None
self
.update()
def
setCurrentTime(
self
, cur_time):
self
.cur_time
=
cur_time
idx
=
0
for
i, (t, _)
in
enumerate
(
self
.lyrics):
if
cur_time >
=
t:
idx
=
i
else
:
break
if
idx !
=
self
.current_index:
self
.current_index
=
idx
self
.update()
else
:
self
.update()
def
paintEvent(
self
, event):
painter
=
QPainter(
self
)
painter.setRenderHint(QPainter.Antialiasing)
w, h
=
self
.width(),
self
.height()
lines
=
[]
for
offset
in
range
(
-
2
,
3
):
idx
=
self
.current_index
+
offset
if
0
<
=
idx <
len
(
self
.lyrics):
lines.append((offset,
self
.lyrics[idx][
1
]))
total_lines
=
len
(lines)
if
total_lines
=
=
0
:
if
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.AlignHCenter | Qt.AlignVCenter,
self
.demo_text)
return
max_line_height
=
h
/
/
max
(total_lines,
1
)
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_height
while
main_font_size >
10
:
font_main
=
QFont(
"微软雅黑"
, main_font_size, QFont.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
:
break
main_font_size
-
=
1
font_main
=
QFont(
"微软雅黑"
, main_font_size, QFont.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
=
None
if
hasattr
(
self
,
"parent"
)
and
hasattr
(
self
.parent(),
"vlc_player"
):
cur_time
=
self
.parent().vlc_player.get_time()
/
1000
elif
hasattr
(
self
,
"cur_time"
):
cur_time
=
self
.cur_time
else
:
cur_time
=
0
cur_idx
=
self
.current_index
cur_line_time
=
self
.lyrics[cur_idx][
0
]
if
self
.lyrics
else
0
next_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
scroll_offset
=
-
percent
*
line_height
start_y
=
(h
-
total_lines
*
line_height)
/
/
2
+
scroll_offset
for
i, (offset, text)
in
enumerate
(lines):
y
=
start_y
+
i
*
line_height
+
line_height
/
/
2
if
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.AlignHCenter | Qt.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.InternalMove)
self
.setContextMenuPolicy(Qt.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.FramelessWindowHint |
Qt.WindowStaysOnTopHint |
Qt.Tool
)
self
.setAttribute(Qt.WA_TranslucentBackground)
self
.setWindowOpacity(
0.85
)
self
.lyric
=
FloatingLyricLabel()
layout
=
QVBoxLayout()
layout.setContentsMargins(
16
,
16
,
16
,
16
)
layout.addWidget(
self
.lyric)
self
.setLayout(layout)
self
.resize(
800
,
100
)
desktop
=
QApplication.desktop()
self
.move(
(desktop.width()
-
self
.width())
/
/
2
,
desktop.height()
-
150
)
self
._move_drag
=
False
self
._move_DragPosition
=
None
self
._resize_drag
=
False
self
._resize_dir
=
None
def
paintEvent(
self
, event):
painter
=
QPainter(
self
)
painter.setRenderHint(QPainter.Antialiasing)
rect
=
self
.rect()
color
=
QColor(
30
,
30
,
30
,
200
)
painter.setBrush(color)
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(rect,
18
,
18
)
def
mousePressEvent(
self
, event):
if
event.button()
=
=
Qt.LeftButton:
pos
=
event.pos()
margin
=
self
.EDGE_MARGIN
rect
=
self
.rect()
if
pos.x() < margin
and
pos.y() < margin:
self
._resize_drag
=
True
self
._resize_dir
=
'topleft'
elif
pos.x() > rect.width()
-
margin
and
pos.y() < margin:
self
._resize_drag
=
True
self
._resize_dir
=
'topright'
elif
pos.x() < margin
and
pos.y() > rect.height()
-
margin:
self
._resize_drag
=
True
self
._resize_dir
=
'bottomleft'
elif
pos.x() > rect.width()
-
margin
and
pos.y() > rect.height()
-
margin:
self
._resize_drag
=
True
self
._resize_dir
=
'bottomright'
elif
pos.x() < margin:
self
._resize_drag
=
True
self
._resize_dir
=
'left'
elif
pos.x() > rect.width()
-
margin:
self
._resize_drag
=
True
self
._resize_dir
=
'right'
elif
pos.y() < margin:
self
._resize_drag
=
True
self
._resize_dir
=
'top'
elif
pos.y() > rect.height()
-
margin:
self
._resize_drag
=
True
self
._resize_dir
=
'bottom'
else
:
self
._move_drag
=
True
self
._move_DragPosition
=
event.globalPos()
-
self
.pos()
event.accept()
def
mouseMoveEvent(
self
, event):
if
self
._move_drag
and
event.buttons()
=
=
Qt.LeftButton:
self
.move(event.globalPos()
-
self
._move_DragPosition)
event.accept()
elif
self
._resize_drag
and
event.buttons()
=
=
Qt.LeftButton:
gpos
=
event.globalPos()
geo
=
self
.geometry()
minw, minh
=
200
,
50
if
self
._resize_dir
=
=
'left'
:
diff
=
gpos.x()
-
geo.x()
neww
=
geo.width()
-
diff
if
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()
-
diff
if
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()
-
diffx
newh
=
geo.height()
-
diffy
if
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()
-
diffy
if
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()
-
diffx
newh
=
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.pos())
def
mouseReleaseEvent(
self
, event):
self
._move_drag
=
False
self
._resize_drag
=
False
self
._resize_dir
=
None
def
setLyrics(
self
, lyrics):
self
.lyric.setLyrics(lyrics)
def
setCurrentTime(
self
, time):
self
.lyric.setCurrentTime(time)
def
update_cursor(
self
, pos):
margin
=
self
.EDGE_MARGIN
rect
=
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.SizeFDiagCursor)
elif
(pos.x() > rect.width()
-
margin
and
pos.y() < margin)
or
(pos.x() < margin
and
pos.y() > rect.height()
-
margin):
self
.setCursor(Qt.SizeBDiagCursor)
elif
pos.x() < margin
or
pos.x() > rect.width()
-
margin:
self
.setCursor(Qt.SizeHorCursor)
elif
pos.y() < margin
or
pos.y() > rect.height()
-
margin:
self
.setCursor(Qt.SizeVerCursor)
else
:
self
.setCursor(Qt.ArrowCursor)
def
enterEvent(
self
, event):
self
.update_cursor(
self
.mapFromGlobal(QCursor.pos()))
def
leaveEvent(
self
, event):
self
.setCursor(Qt.ArrowCursor)
class
FloatingLyricLabel(LyricLabel):
def
paintEvent(
self
, event):
painter
=
QPainter(
self
)
painter.setRenderHint(QPainter.Antialiasing)
w, h
=
self
.width(),
self
.height()
lines
=
[]
idx
=
self
.current_index
count
=
0
while
idx <
len
(
self
.lyrics)
and
count <
2
:
text
=
self
.lyrics[idx][
1
].strip()
if
text:
lines.append(text)
count
+
=
1
idx
+
=
1
while
len
(lines) <
2
:
lines.append("")
if
not
any
(lines):
if
hasattr
(
self
,
"demo_text"
)
and
self
.demo_text:
font
=
QFont(
"微软雅黑"
,
int
(h
*
0.35
), QFont.Bold)
painter.setFont(font)
painter.setPen(QColor(
"#aaa"
))
painter.drawText(
0
,
0
, w, h, Qt.AlignHCenter | Qt.AlignVCenter,
self
.demo_text)
return
main_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_size
while
font_size >
10
:
font
=
QFont(
"微软雅黑"
, font_size, QFont.Bold
if
bold
else
QFont.Normal)
painter.setFont(font)
rect
=
painter.fontMetrics().boundingRect(text)
if
rect.width() <
=
max_width
*
0.95
and
rect.height() <
=
max_height
*
0.95
:
break
font_size
-
=
1
return
font_size
main_ratio
=
0.58
next_ratio
=
0.28
gap
=
int
(h
*
0.08
)
main_max_height
=
h
*
main_ratio
next_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.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
+
gap
start_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.AlignHCenter | Qt.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.AlignHCenter | Qt.AlignVCenter, next_text)
class
CustomTitleBar(QWidget):
def
__init__(
self
, parent
=
None
):
super
().__init__(parent)
self
.setFixedHeight(
40
)
self
.setStyleSheet(
)
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.LeftButton:
self
._drag_pos
=
event.globalPos()
-
self
.window().frameGeometry().topLeft()
event.accept()
def
mouseMoveEvent(
self
, event):
if
event.buttons()
=
=
Qt.LeftButton:
self
.window().move(event.globalPos()
-
self
._drag_pos)
event.accept()
def
paintEvent(
self
, event):
super
().paintEvent(event)
painter
=
QPainter(
self
)
painter.setRenderHint(QPainter.Antialiasing)
pen
=
QPen(QColor(
17
,
17
,
17
),
2
)
painter.setPen(pen)
y
=
self
.height()
+
4
painter.drawLine(
10
, y,
self
.width()
-
10
, y)
class
MP3Player(QWidget):
def
__init__(
self
):
super
().__init__()
self
.setWindowFlags(Qt.FramelessWindowHint)
self
.setAttribute(Qt.WA_TranslucentBackground)
self
.song_list
=
[]
self
.current_index
=
-
1
self
.player
=
QMediaPlayer()
self
.vlc_player
=
vlc.MediaPlayer()
self
.timer
=
QTimer(
self
)
self
.timer.setInterval(
500
)
self
.timer.timeout.connect(
self
.update_progress)
self
.slider_is_pressed
=
False
self
.loop_mode
=
False
self
.shuffle_mode
=
False
self
.default_lyric_text
=
"深埋生命血脉相连\n用丝绸去润泽你的肌肤"
self
.floating_lyric
=
FloatingLyricWindow()
self
.floating_lyric_visible
=
False
self
.is_muted
=
False
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"
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.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_add_file
=
QPushButton(
"添加文件"
)
self
.btn_add_dir
=
QPushButton(
"添加目录"
)
self
.btn_save_list
=
QPushButton(
"保存列表"
)
self
.btn_load_list
=
QPushButton(
"加载列表"
)
self
.btn_mute
=
QPushButton(
"🔊"
)
self
.btn_float_lyric
=
QPushButton(
"词"
)
button_style
=
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
=
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_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_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)
self
.setLayout(QVBoxLayout())
self
.layout().addWidget(main_widget)
self
.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
=
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
=
row
self
.play_song_by_index(row)
def
play_selected(
self
):
row
=
self
.list_widget.currentRow()
self
.sync_song_list()
if
row >
=
0
:
self
.current_index
=
row
state
=
self
.vlc_player.get_state()
if
state
in
(vlc.State.Playing, vlc.State.Buffering):
self
.vlc_player.pause()
elif
state
=
=
vlc.State.Paused:
self
.vlc_player.play()
elif
state
in
(vlc.State.Stopped, vlc.State.Ended, vlc.State.NothingSpecial):
self
.play_song_by_index(row)
def
play_song_by_index(
self
, idx):
if
0
<
=
idx <
len
(
self
.song_list):
self
.current_index
=
idx
song_path
=
self
.song_list[idx]
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(
"--:--"
)
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
=
parsed
self
.lyric.setLyrics(
self
.lyrics_parsed)
self
.floating_lyric.setLyrics(
self
.lyrics_parsed)
else
:
self
.lyric.setDemoText(
self
.default_lyric_text)
self
.floating_lyric.setLyrics([])
else
:
self
.lyric.setDemoText(
self
.default_lyric_text)
self
.floating_lyric.setLyrics([])
except
Exception as e:
self
.lyric.setDemoText(
self
.default_lyric_text)
self
.floating_lyric.setLyrics([])
def
play_prev(
self
):
if
self
.song_list:
if
self
.shuffle_mode:
import
random
candidates
=
[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_index
else
:
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
random
candidates
=
[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_index
else
:
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
=
True
def
on_slider_released(
self
):
self
.slider_is_pressed
=
False
total
=
self
.vlc_player.get_length()
if
total >
0
:
pos
=
self
.slider.value()
/
100
self
.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
random
if
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_index
self
.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(
"⏸"
)
else
:
self
.btn_play.setText(
"▶"
)
if
hasattr
(
self
,
"lyric"
)
and
hasattr
(
self
,
"lyrics_parsed"
):
cur
=
self
.vlc_player.get_time()
/
1000
self
.lyric.setCurrentTime(cur)
self
.floating_lyric.setCurrentTime(cur)
def
ms_to_mmss(
self
, ms):
s
=
int
(ms
/
/
1000
)
m
=
s
/
/
60
s
=
s
%
60
return
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_mode
self
.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)
self
.append_to_fav(
self
.song_list[
self
.current_index])
def
update_shuffle_button_style(
self
):
if
self
.shuffle_mode:
self
.btn_loop.setText(
"🔀"
)
else
:
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
None
def
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.text
except
Exception:
pass
return
None
def
toggle_floating_lyric(
self
):
if
self
.floating_lyric_visible:
self
.floating_lyric.hide()
self
.floating_lyric_visible
=
False
self
.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
=
True
self
.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_muted
self
.vlc_player.audio_set_mute(
self
.is_muted)
if
self
.is_muted:
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.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:
return
with
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
+
second
text
=
m.group(
4
).strip()
result.append((time, text))
result.sort()
return
result
if
__name__
=
=
"__main__"
:
app
=
QApplication(sys.argv)
player
=
MP3Player()
player.show()
print
(
"当前工作目录:"
, os.getcwd())
sys.exit(app.exec_())