fuocore¶
fuocore 提供了音乐播放器依赖的一些常见模块, 它主要是为 feeluown 播放器而设计的, 所以名字为 fuocore,意为 feeluown core。 它主要包含以下几个部分:
- 音乐资源管理模块:抽象、统一并管理各音乐平台资源
- 播放器模块:播放媒体资源
- 歌词模块:歌词解析、计算实时歌词等
- 一些工具:PubsubServer、TcpServer 等
注解
理论上其它音乐播放器也可以使用 fuocore 作为其基础模块。
此外,fuocore 也包含了网易云音乐、虾米音乐、本地音乐的扩展,实现了对这些平台的音乐资源的访问, 并根据音乐资源管理模块对其进行了抽象和管理。 以这几个扩展为参考样例,我们可以轻松的编写 last.fm、豆瓣 FM、 soundcloud 等音乐平台的扩展,欢迎有兴趣的童鞋一起来 hack。
使用示例¶
>>> # use MpvPlayer to play mp3
>>> from fuocore.player import MpvPlayer
>>> player = MpvPlayer()
>>> player.initialize()
>>> player.play('./data/fixtures/ybwm-ts.mp3')
>>> player.stop()
>>> # play netease song
>>> from fuocore.netease import provider
>>> result = provider.search('谢春花')
>>> song = result.songs[0]
>>> print(song.title)
>>> player.play(song.url)
>>> # show live lyric
>>> from fuocore.live_lyric import LiveLyric
>>> player.stop()
>>> live_lyric = LiveLyric()
>>> player.position_changed.connect(live_lyric.on_position_changed)
>>> player.playlist.song_changed.connect(live_lyric.on_song_changed)
>>> player.play_song(song)
>>> def cb(s):
... print(s)
...
>>> live_lyric.sentence_changed.connect(cb)
注解
fuocore 提供的 MpvPlayer 是依赖 mpv 播放器的,在 Debian 发行版中,我们需要安装 libmpv1, 在 macOS 上,我们需要安装 mpv 播放器。
sudo apt-get install libmpv1 # Debian or Ubuntu
brew install mpv # macOS
模块设计¶
fuocore 是 feeluown core 的缩写,它是 feeluown 的核心模块。 而 fuocore 又由几个小模块组成,它们分别是:
音乐库模块
:对各平台音乐资源进行抽象及统一播放器模块
:音乐播放器歌词模块
:歌词解析
另外,fuocore 也包含了几个工具类
- asyncio/threading tcp server
- signal/slot
- pubsub server
注解
Q: 部分基础组件已经有更好的开源实现,为什么我们这里要重新造了一个轮子? 比如 tcp server,Python 标准库 Lib/socketserver.py 中提供了更加完善的 TcpServer 实现。
A: 我也不知道为啥,大概是为了学习和玩耍,嘿嘿~ 欢迎大家对它们进行优化, 希望大家享受造轮子和改进轮子的过程。
音乐库模块¶
音乐库模块由几个部分组成:音乐对象模型(Model)、音乐提供方(Provider)、提供方管理(Library)。
音乐对象模型¶
音乐库模块最重要的部分就是定义了音乐相关的一些领域模型 。 比如歌曲模型、歌手模型、专辑模型等。其它各模块都会依赖这些模型。 这些模型在代码中的体现就是一个个 Model,这些 Model 都定义在 models.py 中, 举个例子,我们对歌曲模型的定义为:
class SongModel(BaseModel):
class Meta:
model_type = ModelType.song.value
# TODO: 支持低/中/高不同质量的音乐文件
fields = ['album', 'artists', 'lyric', 'comments', 'title', 'url',
'duration', ]
@property
def artists_name(self):
return ','.join((artist.name for artist in self.artists))
@property
def album_name(self):
return self.album.name if self.album is not None else ''
@property
def filename(self):
return '{} - {}.mp3'.format(self.title, self.artists_name)
def __str__(self):
return 'fuo://{}/songs/{}'.format(self.source, self.identifier) # noqa
def __eq__(self, other):
return all([other.source == self.source,
other.identifier == self.identifier])
这个模型的核心意义在于:它定义了一首歌曲 有且只有 某些属性, 其它模块可以依赖这些属性 。
举个例子,在程序的其它模块中,当我们遇到 song 对象时,我们可以确定,
这个对象一定 会有 title 属性 。如上所示,还有 album
, artists
, lyric
,
comments
, artists_name
等属性。
虽然这些模型一定会有这些属性,但我们访问时仍然要注意两点。
1. 属性的值不一定是可用的 。比如一首歌可能没有歌词,
也没有评论,这时,我们访问这首歌 song.lyric
时,得到的就是一个空字符串,
访问 song.comments
属性时,得到的是一个空列表,但不会是 None 。
2. 第一次获取属性的值的时候可能会产生网络请求 。以歌曲为例,
第一次获取歌曲 url
, lyric
, comments
等属性时,往往会产生网络请求;
以专辑为例,第一次获取专辑的 songs 属性时,往往也会产生网络请求,而第二次获取
就会非常快。
待处理
第二点也隐含一个问题: 获取一个 Model 某个字段的值的行为变得不可预期 。
这个设计对开发者来说有利有弊,这样设计的缘由 怎样较好的抽象不同的资源提供方? 欢迎大家对此进行讨论,提出更好的方案。
provider¶
每个音乐提供方都对应一个 provider,provider 是我们访问具体一个音乐平台资源音乐的入口。
举个栗子,如果我们要从网易云音乐中搜索音乐:
from fuocore.netease import provider
result = provider.search('keyword')
再举另外一个栗子,如果我们知道网易云音乐一首歌曲的 id,我们可以通过下面的方式来获取音乐详细信息:
from fuocore.netease import provider
song = provider.Song.get(song_id)
如果我们需要以特定的身份来访问音乐资源,我们可以使用 provider 的 auth
方法:
from fuocore.netease import provider
user_a = obj # UserModel
provider.auth(user_a)
# 使用 auth_as 来临时切换用户身份
with provider.auth_as(user_b):
provider.Song.get(song_id)
对外接口¶
这部分文档覆盖了 fuocore 对外提供的所有模块和接口。
-
class
fuocore.models.
BaseModel
(*args, **kwargs)[源代码]¶ Base model for music resource.
参数: identifier – model object identifier, unique in each provider
变量: - allow_get – meta var, whether model has a valid get method
- allow_list – meta var, whether model has a valid list method
-
class
fuocore.models.
SongModel
(*args, **kwargs)[源代码]¶ Song Model
参数: - title (str) – song title
- url (str) – song url (http url or local filepath)
- duration (float) – song duration
- album (AlbumModel) – album which song belong to
- artists (list) – song artists
ArtistModel
- lyric (LyricModel) – song lyric
-
class
fuocore.models.
LyricModel
(*args, **kwargs)[源代码]¶ Lyric Model
参数: - song (SongModel) – song which lyric belongs to
- content (str) – lyric content
- trans_content (str) – translated lyric content
-
class
fuocore.models.
AlbumModel
(*args, **kwargs)[源代码]¶ Album Model
参数: - name (str) – album name
- cover (str) – album cover image url
- songs (list) – album songs
- artists (list) – album artists
- desc (str) – album description
-
class
fuocore.models.
ArtistModel
(*args, **kwargs)[源代码]¶ Artist Model
参数: - name (str) – artist name
- cover (str) – artist cover image url
- songs (list) – artist songs
- desc (str) – artist description
-
class
fuocore.models.
PlaylistModel
(*args, **kwargs)[源代码]¶ Playlist Model
参数: - name – playlist name
- cover – playlist cover image url
- desc – playlist description
- songs – playlist songs
-
class
fuocore.models.
UserModel
(*args, **kwargs)[源代码]¶ User Model
参数: - name – user name
- playlists – playlists created by user
- fav_playlists – playlists collected by user
- fav_songs – songs collected by user
- fav_albums – albums collected by user
- fav_artists – artists collected by user
-
class
fuocore.models.
SearchModel
(*args, **kwargs)[源代码]¶ Search Model
参数: - q – search query string
- songs – songs in search result
TODO: support album and artist
-
class
fuocore.provider.
AbstractProvider
[源代码]¶ abstract music resource provider
-
Song
¶
-
Artist
¶
-
Album
¶
-
Playlist
¶
-
Lyric
¶
-
User
¶
-
identifier
¶ provider identify
-
name
¶ provider name
-
-
class
fuocore.player.
PlaybackMode
[源代码]¶ Playlist playback mode.
-
one_loop
= 0¶ One Loop
-
sequential
= 1¶ Sequential
-
loop
= 2¶ Loop
-
random
= 3¶ Random
-
-
class
fuocore.player.
Playlist
(songs=None, playback_mode=<PlaybackMode.loop: 2>)[源代码]¶ player playlist provide a list of song model to play
NOTE - Design: Why we use song model instead of url? Theoretically, using song model may increase the coupling. However, simple url do not obtain enough metadata.
-
playback_mode_changed
= None¶ playback mode changed signal
-
song_changed
= None¶ current song changed signal
-
remove
(song)[源代码]¶ Remove song from playlist. O(n)
If song is current song, remove the song and play next. Otherwise, just remove it.
-
current_song
¶ current playing song, return None if there is no current song
-
next_song
¶ next song for player, calculated based on playback_mode
-
previous_song
¶ previous song for player to play
NOTE: not the last played song
-
-
class
fuocore.live_lyric.
LiveLyric
[源代码]¶ live lyric
LiveLyric listens to song changed signal and position changed signal and emit sentence changed signal. It also has a
current_sentence
property.Usage:
live_lyric = LiveLyric() player.song_changed.connect(live_lyric.on_song_changed) player.position_change.connect(live_lyric.on_position_changed)
-
current_sentence
¶ get current lyric sentence
-
一些调研与思考¶
怎样较好的抽象不同的资源提供方?¶
fuocore 的一个主要目标就是将各个资源进行抽象,统一上层使用资源的方式。 但各个资源提供方提供的 API 差异较大,功能差别不小,比如网易云音乐 会提供批量接口(根据歌曲 id 批量获取歌曲详情),而虾米和 QQ 音乐就没有 类似接口,这给 fuocore 的实现和设计带来了挑战。
问题一:同一平台,不同接口的返回信息有的比较完整,有的不完整¶
我们以网易云音乐的 专辑详细信息接口 和 搜索接口 为例。 它的搜索接口返回的专辑信息大致如下:
"album": {
"artist": {
"id": 0,
"alias": [],
"img1v1": 0,
"name": "",
"picUrl": null,
"picId": 0,
},
"id": 2960228,
"name": "\u5218\u5fb7\u534e Unforgettable Concert 2010",
"picId": 2540971374328644,
...
}
它没有专辑封面的链接,也没有专辑歌曲、歌手信息也不完整信息。 而专辑详细信息接口中,它就有 songs , artists , picUrl 等信息。
面对这个问题,目前有两种解决方案:
- 将搜索接口返回的 Album 定义为 BriefAlbumModel,将详细接口返回的定义为 AlbumModel
- pros: 清晰明了,两者有明显的区分
- cons: 多一个 Model 就多一个概念,上层要对两者进行区分,代码更复杂
- 定义一个 AlbumModel,创建 Model 实例的时候不要求所有字段都有值,
一些字段的值在之后在被真正用到的时候再自动获取。
- cons: 比较隐晦
- cons: 上层不也方便确认哪些字段是已经有值了,哪些会在调用的时候获取
为了让整体代码更简单,目前使用的是第二种方案。上层假设 identifier/name 等字段是一开始就有了,url/artists 等字段需要之后调用接口才会有值。 (尽管这种方案看起来也有明显的缺点,但目前看来可以接受,也没想到更好的方法。欢迎大家讨论新的方案)。
问题二:不同平台,同一接口的返回信息有的比较完整,有的不完整¶
在虾米音乐中,在获取歌曲详细信息的时候,就可以获取这歌曲的播放链接。 但是在网易云音乐,需要单独调用一个接口来获取歌曲的播放链接。
在虾米音乐的 API 中,要获取一个歌手的所有信息,我们需要调用它的多个接口: 一个是歌手详情接口;另一个是歌手歌曲详情接口;还有歌手专辑接口等。
问题三:平台能力方面和开发体验¶
另一方面,就算各音乐平台都提供一样的 API,开发者在开发相关插件的时候, 也不一定会一次性把所有功能都完成,那时,也会存在一个问题: A 插件有某功能,但是 B 插件没有。
所以,当 B 插件没有该功能的时候,系统内部应该怎样处理?又怎样将该 问题呈现给用户呢?
举个例子,对于网易云音乐来说,它的批量获取歌曲功能可以这样实现:
NeteaseSongModel.list(song_ids): -> list<SongModel>
但是我们不能给虾米音乐和 QQ 音乐实现这样的功能,那怎么办, 目前有如下方法:
1. XiamiSongModel.list(song_ids): -> raise NotSupportedError
2. XiamiSongModel.list -> AttributeError # 好像不太优雅
3. XiamiSongModel.allow_batch -> 加一个标记字段
目前使用的是第三种方案,加一个标记字段, allow_get
和 allow_batch
。
相关专业术语¶
- provider
- a music platform that provide music metadata