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 又由几个小模块组成,它们分别是:

  1. 音乐库模块 :对各平台音乐资源进行抽象及统一
  2. 播放器模块 :音乐播放器
  3. 歌词模块 :歌词解析

另外,fuocore 也包含了几个工具类

  1. asyncio/threading tcp server
  2. signal/slot
  3. 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
classmethod get(identifier)[源代码]

获取 model 详情

这个方法必须尽量初始化所有字段,确保它们的值不是 None。

classmethod list(identifier_list)[源代码]

Model batch get 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
add(song_id)[源代码]

add song to playlist, return true if succeed.

If the song was in playlist already, return true.

remove(song_id)[源代码]

remove songs from playlist, return true if succeed

If song is not in playlist, return true.

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
add_to_fav_songs(song_id)[源代码]

add song to favorite songs, return True if success

参数:song_id – song identifier
返回:Ture if success else False
返回类型:boolean
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

fuocore.models.SongModel 的别名

Artist

fuocore.models.ArtistModel 的别名

Album

fuocore.models.AlbumModel 的别名

Playlist

fuocore.models.PlaylistModel 的别名

Lyric

fuocore.models.LyricModel 的别名

User

fuocore.models.UserModel 的别名

identifier

provider identify

name

provider name

auth_as(user)[源代码]

auth as a user temporarily

auth(user)[源代码]

use provider as a specific user

class fuocore.player.State[源代码]

Player states.

stopped = 0
paused = 1
playing = 2
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

add(song)[源代码]

往播放列表末尾添加一首歌曲

insert(song)[源代码]

在当前歌曲后插入一首歌曲

remove(song)[源代码]

Remove song from playlist. O(n)

If song is current song, remove the song and play next. Otherwise, just remove it.

clear()[源代码]

remove all songs from playlists

list()[源代码]

get all songs in playlists

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

on_position_changed(position)[源代码]

bind position changed signal with this

on_song_changed(song)[源代码]

bind song changed signal with this

一些调研与思考

怎样较好的抽象不同的资源提供方?

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 等信息。

面对这个问题,目前有两种解决方案:

  1. 将搜索接口返回的 Album 定义为 BriefAlbumModel,将详细接口返回的定义为 AlbumModel
    • pros: 清晰明了,两者有明显的区分
    • cons: 多一个 Model 就多一个概念,上层要对两者进行区分,代码更复杂
  2. 定义一个 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_getallow_batch

相关专业术语

provider
a music platform that provide music metadata