# type: ignore
import time
import logging
import warnings
from feeluown.media import MultiQualityMixin, Quality
from feeluown.utils.utils import elfhash
from .base import ModelType, AlbumType, Model, ModelFlags
logger = logging.getLogger(__name__)
def _get_artists_name(artists):
# [a, b, c] -> 'a, b & c'
artists_name = ', '.join((artist.name for artist in artists))
return ' & '.join(artists_name.rsplit(', ', 1))
[docs]class BaseModel(Model):
"""Base model for music resource"""
class Meta:
"""Model metadata"""
flags = ModelFlags.none # flags should help us upgrade to model v2 gracefully
allow_get = True #: whether model has a valid get method
allow_list = False #: whether model has a valid list method
model_type = ModelType.dummy.value
#: declare model fields, each model must have an identifier field
fields = ['identifier', '__cache__']
#: Model 用来展示的字段
fields_display = []
#: 不触发 get 的 Model 字段,这些字段往往 get 是获取不到的
fields_no_get = ['identifier', '__cache__']
def __eq__(self, other):
if not isinstance(other, BaseModel):
return False
return all([other.source == self.source,
str(other.identifier) == str(self.identifier),
other.meta.model_type == self.meta.model_type])
[docs] @classmethod
def get(cls, identifier):
"""get model instance by identifier"""
@classmethod
def list(cls, identifier_list):
"""Model batch get method"""
def cache_get(self, key):
self._init_cache()
if key in self.__cache__:
value, expired_at = self.__cache__[key]
if expired_at is None or expired_at >= int(time.time()):
return value, True
return None, False
def cache_set(self, key, value, ttl=None):
"""
:param int ttl: the unit is seconds.
"""
self._init_cache()
if ttl is None:
expired_at = None
else:
expired_at = int(time.time()) + ttl
self.__cache__[key] = (value, expired_at)
def _init_cache(self):
# not thread safe
if self.__cache__ is None:
self.__cache__ = {}
[docs]class ArtistModel(BaseModel):
"""Artist Model"""
class Meta:
model_type = ModelType.artist.value
fields = ['name', 'cover', 'songs', 'desc', 'albums']
fields_display = ['name']
allow_create_songs_g = False
allow_create_albums_g = False
def __str__(self):
return 'fuo://{}/artists/{}'.format(self.source, self.identifier)
[docs] def create_songs_g(self):
"""create songs generator(alpha)"""
pass
def create_albums_g(self):
pass
@property
def aliases(self):
return []
@property
def hot_songs(self): # To be compatible with ArtistModel v2.
return self.songs
@property
def pic_url(self): # To be compatible with ArtistModel v2.
return self.cover
@property
def description(self): # To be compatible with ArtistModel v2.
return self.desc
def __getattribute__(self, name):
value = super().__getattribute__(name)
if name == 'songs':
warnings.warn('please use/implement .create_songs_g')
return value
[docs]class AlbumModel(BaseModel):
class Meta:
model_type = ModelType.album.value
# TODO: 之后可能需要给 Album 多加一个字段用来分开表示 artist 和 singer
# 从意思上来区分的话:artist 是专辑制作人,singer 是演唱者
# 像虾米音乐中,它即提供了专辑制作人信息,也提供了 singer 信息
fields = ['name', 'cover', 'songs', 'artists', 'desc', 'type']
fields_display = ['name', 'artists_name']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if kwargs.get('type') is None:
name = kwargs.get('name')
if name:
self.type = AlbumType.guess_by_name(name)
else:
self.type = AlbumType.standard
def __str__(self):
return 'fuo://{}/albums/{}'.format(self.source, self.identifier)
@property
def artists_name(self):
return _get_artists_name(self.artists or [])
@property
def description(self): # To be compatible with AlbumModel v2.
return self.desc
@property
def type_(self): # To be compatible with AlbumModel v2.
return self.type
[docs]class LyricModel(BaseModel):
"""Lyric Model
:param SongModel song: song which lyric belongs to
:param str content: lyric content
:param str trans_content: translated lyric content
"""
class Meta:
model_type = ModelType.lyric.value
fields = ['song', 'content', 'trans_content']
[docs]class MvModel(BaseModel, MultiQualityMixin):
QualityCls = Quality.Video
class Meta:
model_type = ModelType.video.value
fields = ['name', 'media', 'desc', 'cover', 'artists', 'duration']
support_multi_quality = False
fields_display = ['name']
@property
def title(self):
"""
V2 VideoModel use `title` instead of `name`.
"""
return self.name
@title.setter
def title(self, value):
self.name = value
@property
def title_display(self):
"""
To be compatible with VideoModel v2.
"""
return self.name_display
[docs]class SongModel(BaseModel, MultiQualityMixin):
QualityCls = Quality.Audio
class Meta:
model_type = ModelType.song.value
fields = ['album', 'artists', 'lyric', 'comments', 'title', 'url',
'duration', 'mv', 'media']
fields_display = ['title', 'artists_name', 'album_name', 'duration_ms']
support_multi_quality = False
@property
def artists_name(self):
return _get_artists_name(self.artists or [])
@property
def album_name(self):
return self.album.name if self.album is not None else ''
@property
def duration_ms(self):
if self.duration is not None:
seconds = self.duration / 1000
m, s = seconds / 60, seconds % 60
else:
m, s = 0, 0
return '{:02}:{:02}'.format(int(m), int(s))
@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 __hash__(self):
try:
id_hash = int(self.identifier)
except ValueError:
id_hash = elfhash(self.identifier.encode())
return id_hash * 1000 + id(type(self)) % 1000
def __eq__(self, other):
if not isinstance(other, SongModel):
return False
return all([other.source == self.source,
str(other.identifier) == str(self.identifier)])
[docs]class PlaylistModel(BaseModel):
class Meta:
model_type = ModelType.playlist.value
fields = ['name', 'cover', 'songs', 'desc']
fields_display = ['name']
allow_create_songs_g = False
def __str__(self):
return 'fuo://{}/playlists/{}'.format(self.source, self.identifier)
@property
def creator(self): # To be compatible with PlaylistModel v2.
return None
@property
def creator_name(self): # To be compatible with PlaylistModel v2.
return ''
def __getattribute__(self, name):
value = super().__getattribute__(name)
if name == 'songs':
warnings.warn('please use/implement .create_songs_g')
return value
[docs] def add(self, song_id):
"""add song to playlist, return true if succeed.
If the song was in playlist already, return true.
"""
pass
[docs] def remove(self, song_id):
"""remove songs from playlist, return true if succeed
If song is not in playlist, return true.
"""
pass
def create_songs_g(self):
pass
@property
def description(self): # To be compatible with ArtistModel v2.
return self.desc
[docs]class SearchModel(BaseModel):
"""Search Model
TODO: support album and artist
"""
class Meta:
model_type = ModelType.dummy.value
# XXX: songs should be a empty list instead of None
# when there is not song.
fields = ['q', 'songs', 'playlists', 'artists', 'albums', 'videos']
fields_no_get = ['q', 'songs', 'playlists', 'artists', 'albums', 'videos']
def __str__(self):
return 'fuo://{}?q={}'.format(self.source, self.q)
[docs]class UserModel(BaseModel):
"""User Model
:param name: user name
:param playlists: playlists created by user
:param fav_playlists: playlists collected by user
:param fav_songs: songs collected by user
:param fav_albums: albums collected by user
:param fav_artists: artists collected by user
"""
class Meta:
allow_fav_songs_add = False
allow_fav_songs_remove = False
allow_fav_playlists_add = False
allow_fav_playlists_remove = False
allow_fav_albums_add = False
allow_fav_albums_remove = False
allow_fav_artists_add = False
allow_fav_artists_remove = False
model_type = ModelType.user.value
fields = ['name', 'avatar_url', 'playlists', 'fav_playlists', 'fav_songs',
'fav_albums', 'fav_artists', 'rec_songs', 'rec_playlists']
fields_display = ['name']
[docs] def add_to_fav_songs(self, song_id):
"""add song to favorite songs, return True if success
:param song_id: song identifier
:return: Ture if success else False
:rtype: boolean
"""
pass
[docs] def remove_from_fav_songs(self, song_id):
pass
[docs] def add_to_fav_playlists(self, playlist_id):
pass
[docs] def remove_from_fav_playlists(self, playlist_id):
pass
[docs] def add_to_fav_albums(self, album_id):
pass
[docs] def remove_from_fav_albums(self, album_id):
pass
[docs] def add_to_fav_artists(self, aritst_id):
pass
[docs] def remove_from_fav_artists(self, artist_id):
pass
class VideoModel(BaseModel):
class Meta:
model_type = ModelType.video.value
fields = ['title', 'cover', 'media']
fields_display = ['title']
def __str__(self):
return f'fuo://{self.source}/videos/{self.identifier}'