Groove 音乐
一个基于 PyQt5 的跨平台音乐播放器
欢迎来到 Groove 文档
本文档是 Groove 项目的说明及帮助文档,包含用户指南和开发者指南两部分。
快速上手
Groove 使用 Python3 进行开发,基于 PyQt5 构建 GUI,在使用之前需要根据操作系统下载相应的解码器。
安装
Win32
安装包
下载并安装 LAV Filters
从 Release 页面下载
Groove_v*.*.*_x64_setup.exe
右键并以管理员身份运行
Groove_v*.*.*_x64_setup.exe
,根据安装向导完成 Groove 的安装开启你的音乐之旅 😊~~
免安装版
下载并安装 LAV Filters
从 Release 页面下载
Groove_v*.*.*_windows_x64.zip
解压
Groove_v*.*.*_windows_x64.zip
在解压出来的
Groove
文件夹中,找到并双击运行 Groove.exe开启你的音乐之旅 😊~~
Linux
安装 GStreamer
从 Release 页面下载
Groove_v*.*.*_linux_x64.zip
解压
Groove_v*.*.*_linux_x64.zip
在解压出来的
Groove
文件夹中,找到并双击运行 Groove 可执行文件开启你的音乐之旅 😊~~
基本使用
播放本地音乐
搜索、播放和下载在线音乐
创建和管理个人播放列表
查看和编辑歌曲元数据
观看和下载在线 MV
音频格式
目前 Groove 音乐支持以下格式的音频,并根据文件的后缀名过滤掉不受支持的文件:
后缀名 | 描述 |
---|---|
*.mp3, *.m4a, *.mp4 | MPEG File |
*.tta | True Audio File |
*.wav | WAVE Audio File |
*.wv | WavPack Audio File |
*.ac3 | Audio Codec 3 File |
*.opus | Ogg Opus Audio File |
*.ogg | Ogg Vorbis Audio File |
*.wma | Windows Media Audio File |
*.aac | Advanced Audio Coding File |
*.asf | Advanced Systems Format File |
*.aiff | Audio Interchange File Format |
*.flac | Free Lossless Audio Codec File |
*.mpc | Musepack Compressed Audio File |
*.ape | Monkey’s Audio Lossless Audio File |
播放逻辑
播放模式
QMediaPlaylist
支持五种播放模式:
枚举成员 | 值 | 描述 |
---|---|---|
QMediaPlaylist::CurrentItemOnce |
0 |
当前媒体只被播放一次 |
QMediaPlaylist::CurrentItemInLoop |
1 |
单曲循环 |
QMediaPlaylist::Sequential |
2 |
顺序播放,播放完不会从头再来 |
QMediaPlaylist::Loop |
3 |
列表循环,播放完从头再来 |
QMediaPlaylist::Random |
4 |
随机播放 |
Groove 音乐支持除了 CurrentItemOnce
外的所有播放模式。
按钮组合
Groove 音乐的播放栏上有两个按钮用来控制播放逻辑,分别是随机播放按钮和循环模式按钮。
随机播放按钮有两种状态: 选中
和 未选中
,循环模式按钮有三种状态: 顺序播放
、列表循环
和 单曲循环
。两种按钮的状态组合与播放器播放模式的对应关系如下表所示:
按钮状态 | 播放模式 |
---|---|
未选中 + 顺序播放 |
Sequential |
未选中 + 列表循环 |
Loop |
未选中 + 单曲循环 |
CurrentItemInLoop |
选中 + 顺序播放 |
Random |
选中 + 列表循环 |
Random |
选中 + 单曲循环 |
CurrentItemInLoop |
当播放模式为 CurrentItemInLoop
时,无论随机播放按钮是否被选中,点击下一首按钮时都会按顺序选中并播放正在播放列表中的下一首歌曲。
快捷键
全局
全局快捷键在 Groove 音乐不处于活跃状态时(比如最小化到托盘)仍可用:
快捷键 | 描述 |
---|---|
⏯️ | 切换播放状态 |
⏮️ | 播放上一首 |
⏭️ | 播放下一首 |
局部
局部快捷键只在 Groove 音乐处于活跃状态时(位于所有桌面应用的顶部)可用:
快捷键 | 描述 |
---|---|
→ | 快进 |
← | 快退 |
Esc | 退出全屏 |
空格 | 切换播放状态 |
Ctrl+ | 加快播放速度 |
Ctrl- | 减慢播放速度 |
CtrlEnter | 重置播放速度 |
歌词文件
Groove 音乐支持 lrc
格式和 json
格式的歌词文件。
lrc 格式
歌词格式为 [mm:ss.xx],其中 mm 为分钟,ss 为秒,xx 为百分之一秒,更多关于 lrc
格式的信息可以参见 维基百科。下面是一个例子:
[ti:Lyric Demo]
[ar:zhiyiYo]
[au:Written by zhiyiYo, 2022]
[al:Groove - Vol. 2 – Melody]
[00:12.00]zhiyiYo - Lyric Demo
[00:15.30]hello
[00:15.30]你好 # 重复时间标签来添加翻译
[01:02.30]world
[01:04.29]我家硝子真卡哇伊🥰
json 格式
歌词格式为 “seconds”:[”orginal lyric”] 或者 “seconds”:[”orginal lyric”, “translation lyric”]。如下所示:
{
"1.86": [
"微熱 - Aiko "
],
"3.7": [
"词:aiko"
],
"6.49": [
"曲:aiko"
],
"28.22": [
"今夜も必ず連絡するね",
"今夜也一定会和我联系"
],
"34.36": [
"昼も夜も抱きしめて",
"又能相拥一夜"
],
}
常见问题
为什么窗口拖动的时候会出现卡顿现象?
由于界面使用了亚克力窗口特效,在某些版本的 Win10 上会出现这个问题。有三种解决方案:
更新 Win10 到最新版本,比如 Win11.
取消复选框的选中 高级系统设置 –> 性能 –> 拖动时显示窗口内容.
在设置界面禁用亚克力效果.
为什么运行的时候 GStreamer 报错:Warning: “No decoder available for type …”?
可以尝试 sudo apt-get install gstreamer1.0-libav
来解决该问题,Ubuntu 20.04 亲测有效。
支持哪些格式的歌词文件呀?
目前支持 .lrc
和 .json
格式的歌词文件,更多信息请参见 歌词文件格式说明。
快速上手
搭建开发环境
创建虚拟环境:
conda create -n Groove python=3.8 conda activate Groove pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
下载解码器:
对于 Win32,安装 LAV Filters
对于 Linux,安装 GStreamer
打开 Groove 音乐:
cd app conda activate Groove python Groove.py
VSCode 配置文件
这里提供几个使用 VSCode 开发时会用到的配置文件。
launch.json
launch.json
用来调试 Groove 音乐,需要在 VSCode 中将 Python 解释器切换为 Groove
虚拟环境下的解释器才能保证环境不出问题。
{
"version": "0.2.0",
"configurations": [
{
"name": "调试当前文件",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true,
"cwd": "${fileDirname}"
},
{
"name": "调试 Groove",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/app/Groove.py",
"console": "integratedTerminal",
"justMyCode": true,
}
]
}
tasks.json
tasks.json
用来配置任务,一个任务对应着一条或者多条命令,这里总共配置了三个任务:Run Groove
、Compile qrc
和 Compile qrc and run Groove
:
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Groove",
"detail": "运行 Groove 音乐",
"type": "shell",
"command": "D:/Anaconda/envs/Groove/python.exe",
"args": ["Groove.py"],
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}/app"
}
},
{
"label": "Compile qrc",
"detail": "编译 qrc 文件",
"type": "shell",
"command": "pyrcc5",
"args": [
"-o",
"../common/resource.py",
"resource.qrc",
],
"options": {
"cwd": "${workspaceFolder}/app/resource"
},
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Compile qrc and run Groove",
"detail": "编译 qrc 并运行 Groove 音乐",
"type": "shell",
"command": "D:/Anaconda/envs/Groove/python.exe",
"args": ["Groove.py"],
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"Compile qrc"
],
"options": {
"cwd": "${workspaceFolder}/app"
}
},
]
}
注意事项
资源文件发生变更之后需要使用
pyrcc5
重新编译resource.qrc
文件,生成的resource.py
文件放在common
文件夹下面
软件架构
主要模块
名字 | 模块 |
---|---|
爬虫 | common.crawler |
设置 | common.config.Config |
音乐库 | common.library.Library |
主界面 | View.main_window.MainWindow |
播放器 | components.media_player.MediaPlayer |
播放列表 | components.media_player.MediaPlaylist |
事件总线 | common.signal_bus.SignalBus |
元数据管理 | common.meta_data |
界面结构
主界面
选择模式界面
Groove 音乐中大多数界面的结构如下图所示,由 view
和 SelectionModeBar
组成:
由于 SelectionModeBar
种类多样,代码中使用工厂模式来创建 SelectionModeBar
,这样可以增强代码的可拓展性:
选择模式界面的类图如下所示,SelectionModeInterface
的子类使用 setView(view)
方法更换视图为专辑卡视图、歌曲列表部件、歌手卡视图或者播放列表卡视图,这些视图都实现了 SelectionModeViewBase
的两个抽象方法:
以专辑卡视图为例,AlbumCardViewBase
通过 AlbumCardFactory
创建各种类型的专辑卡,由于专辑卡视图有网格布局和水平布局两种,所以相应地有 GridAlbumCardView
和 HorizonAlbumCardView
子类:
开发规范
命名规范
包名和文件名使用蛇形命名法
类使用大驼峰命名法
函数和变量使用小驼峰命名法,与 Qt 保持一致
项目结构
app
所有与图形界面相关的代码都放在此文件夹下,具体结构如下:
common 文件夹:包含所有文件共享的函数和类
components 文件夹:包含所有窗口共享的组件,比如按钮、菜单和对话框
View 文件夹:包含各个界面,比如我的音乐界面、正在播放界面和主界面
resource 文件夹:包含图标和样式表等资源文件
config 文件夹:包含配置文件
config.json
cache 文件夹:包含缓存的图片、数据库和日志
tests
用于存放测试用例,修改代码后应该再次运行测试用例。
docs
用于存放项目文档,使用说明可以参见 《Sphinx + Read the Docs 从懵逼到入门》
数据库
Groove 音乐使用 sqlite 数据库进行歌曲信息、专辑信息和播放列表信息等数据的管理。
各个模块
entity
实体类模块,每个实体类实例用于保存一条数据表记录。
dao
数据库访问操作模块,DaoBase
作为基类封装了基本的数据库操作方法,使得子类无需编写重复的 SQL 语句就能操作数据库。
service
业务模块,业务类使用 Dao
类来操作数据库
controller
控制器模块,控制器类使用 Service
类来操作数据库,外部使用 controller
提供的接口来访问数据库。
数据表
tbl_song_info
歌曲信息表,对应 SongInfo
实体类:
字段 | 类型 | 描述 |
---|---|---|
file |
str |
文件路径 |
title |
str |
标题 |
singer |
str |
歌手 |
album |
str |
专辑 |
year |
int |
年份 |
genre |
str |
流派 |
duration |
int |
时长(s) |
track |
int |
曲目 |
trackTotal |
int |
专辑曲目总数 |
disc |
int |
光盘 |
discTotal |
int |
光盘总数 |
createTime |
int |
文件创建时间 |
modifiedTime |
int |
文件修改时间 |
其中,file
字段有两种格式:
本地音乐路径,比如:
D:/Music/aiko - 二人.mp3
在线音乐 URL
虚假 URL,比如:http://kuwo/song/2333,播放时会被转换为真实 URL
真实 URL
tbl_album_info
专辑信息表,对应 AlbumInfo
实体类:
字段 | 类型 | 描述 |
---|---|---|
id |
str |
专辑 id |
singer |
str |
歌手 |
album |
str |
专辑名 |
year |
int |
年份 |
genre |
str |
流派 |
modifiedTime |
int |
修改时间 |
tbl_singer_info
歌手信息表,对应 SingerInfo
实体类:
字段 | 类型 | 描述 |
---|---|---|
id |
str |
歌手 id |
singer |
str |
歌手名 |
genre |
str |
流派 |
tbl_playlist
自定义播放列表,对应 Playlist
实体类:
字段 | 类型 | 描述 |
---|---|---|
name |
str |
播放列表名字 |
singer |
str |
第一首歌的歌手名 |
album |
str |
第一首歌的专辑名 |
count |
int |
歌曲数量 |
modifiedTime |
int |
修改时间 |
仔细想想,singer
、album
和 count
字段不应该保存到数据表中,而是在查询的时候填入实体类实例,罢了罢了,软件开发第一原则,程序能跑就行~~
tbl_song_playlist
自定义播放列表和歌曲信息中间表,对应 SongPlaylist
实体类:
字段 | 类型 | 描述 |
---|---|---|
id |
str |
记录 id |
file |
str |
文件路径 |
name |
str |
播放列表名字 |
tbl_recent_play
最近播放表,对应 RecentPlay
实体类:
字段 | 类型 | 描述 |
---|---|---|
file |
str |
文件路径 |
lastPlayedTime |
int |
最近播放时间 |
frequency |
int |
播放次数 |
事件总线
在 Qt 中可以使用信号和槽机制很方便地实现部件之间的通信,考虑下面这样的场景:
如果想要点击任意一个专辑卡并通知主界面跳转到专辑界面,一种实现方式如上图所示:点击任意一个蓝色方框所示的专辑卡,发出 switchToAlbumInterfaceSig
信号给父级部件专辑卡视图,因为专辑卡视图有许多个分组,比如上图中为 aiko
分组,可能还有 柳井爱子
分组,那么这些视图都应该将 switchToAlbumInterfaceSig
转发给父级窗口我的音乐界面,我的音乐界面再转发给主界面,从而实现界面跳转。
可以看到上面这种做法很麻烦,专辑卡上拥有 switchToAlbumInterfaceSig
属性就算了,还要连累父级专辑卡视图以及祖父级我的音乐界面也拥有这个属性才能实现信号的转发。有没有一种方式可以省掉中间的转发过程,从而一步到位通知主界面呢?这就需要使用下面所介绍的全局事件总线思想(这里不区分信号总线和事件总线两种叫法)。
Vue 中的全局事件总线
在 vue 中要实现任意组件间通信,可以在 Vue.prototype
上添加一个全局事件总线 $bus
属性,当组件 A 想要给组件 B 发送一些数据时,只需要在 A 中 this.$bus.$emit(事件名,数据)
发送数据,在 B 中 this.$bus.$on(事件名,回调)
就能通过总线收到数据,而无需借助其他组件的转发。将事件名视为信号,回调视为槽函数,那么这个过程和 Qt 的信号和槽机制神似。
Qt 中的全局事件总线
仿照上述过程,我们来定义一个全局事件总线类,并使用单例模式保证只能实例化出一个对象:
# coding:utf-8
from PyQt5.QtCore import QObject, pyqtSignal
class SignalBus(QObject):
""" 全局事件总线 """
switchToAlbumInterfaceSig = pyqtSignal(str)
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super(SignalBus, cls).__new__(cls, *args, **kwargs)
return cls._instance
bus = SignalBus()
回到最初的那个例子,现在我们只需导入 bus
对象,点击 aikoの詩。
专辑卡时 bus.switchToAlbumInterfaceSig.emit('aiko - aikoの詩。')
来发送切换到专辑界面的信号,然后在主界面中 bus.switchToAlbumInterfaceSig.connect(self.switchToAlbumInterface)
即可,这样就省去了信号的转发流程,代码会简洁许多。
踩过的坑
不能直接给图片添加
.jpg
后缀名,会导致QPixmap
无法识别网格布局的行和列只能增加不能减少,但是可以改变没有用到的行或者列的宽度
要想改变旧布局,只需在一个总的布局中添加后来想要移除的布局就行,比如
all_h_layout.addLayout(gridLayout)
,后面要改变的时候只需removeItem(gridLayout)
deleteLater()
释放内存滚动条最好手动设置最小高度,不然可能太小而看不见
m4a 不存在某个键时需要先手动创建一个空列表,再将值添加到列表中
如果 widget 是自定义类要设置背景颜色首先要添加一句:
self.setAttribute(Qt::WA_StyledBackground, true)
self.setStyleSheet("background-color: rgb(255, 255, 255)")
给主窗口设置磨砂效果,然后留下一部分完全透明的给子部件,这样看起来就好像子部件也打开了磨砂效果
如果需要指定无边框窗体,但是又需要保留操作系统的边框特性,可以自由拉伸边框,可以使用
setWindowFlags(Qt::CustomizeWindowHint)
;使用
raise_()
函数可以使子窗口置顶event.pos()
返回的是事件相对小部件自己的位置可以通过设置最外层的布局
self.all_h_layout.setSizeConstraint(QLayout.SetFixedSize)
来自动调整大小要在小部件上使用磨砂效果只需将其设置成独立窗体,比如
Qt.window
、Qt.popup
可以通过设置已有的属性来直接改变小部件的状态,比如
label.setProperty('text', str)
可以将实例的text设置为想要的str, 而且还可以在一个小部件上设置多个自定义的属性setContentsMargins(int, int, int, int)
的顺序为left、top、right、bottom使用
self.window()
可以直接获取顶层对象当把小部件添加到 groupBox 中时,groupBox 会变成父级
要想动态更新
QListWidget
的 Item 的尺寸只需重写resizeEvent()
的时候item.setSizeHint(QSize())
可以在样式表中用
background:transparent
来替代setAttribute(Qt.WA_TranslucentBackground)
画图用
drawPixmap()
别用drawRect()
,要写字的时候不能painter.setPen(Qt.NoPen)
文件夹的最后一个字符绝对不能是 /
如果出现主界面卡顿,可以通过信号提前结束此时进行的槽函数,将信号连到另一个槽函数来处理
font-weight = 500
时会变为好康的楷体self.pos().x()
和self.x()
得到的结果相同,代表窗体标题栏左上角的全局坐标,self.geometry().x()
得到的是客户区的全局坐标弹出窗口的 qss 不起作用时可以手动
setStyle(QAppliction.style())
使用
adjustSize()
自动调整窗口尺寸QListWidget
使用setViewportMargins()
设置内边距当滚动条背景出现花纹时记得将
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: none;
}
处于省电模式下运行会卡顿
lambda 表达式在执行的时候才会去寻找变量,开循环将按钮的
clicked
信号连接到 lambda 函数需要写成:bt.clicked.connect(lambda checked, x=x: slotFunc(x))
关于
Groove 音乐开发自 2020 年 5 月 1 日,刚开始只是个本地音乐播放器,随着自身知识储备的增加,逐渐加入了联网功能,支持在线音乐、歌词和 MV。在开发这个项目的过程中自己也学到了很多知识,包括但不仅限于:
PyQt 界面开发
数据库
图像处理
设计模式
爬虫
由于作者能力有限,软件难免有些缺陷,大家在使用过程中有遇到任何问题欢迎提 issue,如果喜欢本项目的话也可以点个 ⭐ 以示支持。
Warning
Groove 音乐仅供学习使用,任何人不得将其用于商业及其他非法用途,否则后果自负。
感谢以下项目
zhiyiYo/PyQt-Frameless-Window:一个基于 PyQt5 的跨平台无边框窗口,支持 Win32、Linux 和 MacOS
zhiyiYo/PyQt-Fluent-Widgets:一个 Windows Fluent 风格的组件库
jsmolka/egg-player:一个 Groove 音乐风格的播放器,这个项目的代码写的非常优雅,作者甚至手撕线程池,对本项目的影响非常大,墙裂推荐大家阅读其代码,一定会有不小的收获