Skip to content
This repository has been archived by the owner on Jun 30, 2019. It is now read-only.

Commit

Permalink
Merge e453ca5 into 381b90b
Browse files Browse the repository at this point in the history
  • Loading branch information
cosven committed Oct 16, 2018
2 parents 381b90b + e453ca5 commit e16c59b
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 78 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CHANGELOG

## 2.1.1 (WIP)
- 给 library 添加 `list_song_standby` 接口

## 2.1 (2018-10-08)
- 修复 XUserModel 的问题
- 完善接口文档
Expand Down
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fuocore 使用示例
================

这些使用例子一方面是作为使用参考;另一方面为开发者调试测试提供实例。

在 shell.py 中构造一些典型的使用场景,开发者可以使用 `ipython -i shell.py` 进行一些测试。
39 changes: 39 additions & 0 deletions examples/library_basic_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import logging
from fuocore.library import Library

from fuocore.xiami import provider as xp
from fuocore.netease import provider as np
from fuocore.qqmusic import provider as lp

logging.basicConfig()
logger = logging.getLogger('fuocore')
logger.setLevel(logging.DEBUG)

lib = Library()
lib.register(xp)
lib.register(np)
lib.register(lp)


def test_list_song_standby():
"""
使用 library.list_song_standby 接口
"""
result = xp.search('小小恋歌 新垣结衣', limit=2)
song = result.songs[0] # 虾米音乐没有这首歌的资源
assert song.url == ''

standby_songs = lib.list_song_standby(song)
for index, ss in enumerate(standby_songs): # pylint: disable=all
print(index, ss.source, ss.title, ss.artists_name, ss.url)


def main():
test_list_song_standby()


if __name__ == '__main__':
main()
20 changes: 20 additions & 0 deletions examples/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import logging
from fuocore.library import Library

from fuocore.xiami import provider as xp
from fuocore.netease import provider as np
from fuocore.qqmusic import provider as lp

logging.basicConfig()
logger = logging.getLogger('fuocore')
logger.setLevel(logging.DEBUG)

lib = Library()
lib.register(xp)
lib.register(np)
lib.register(lp)

library = lib
60 changes: 56 additions & 4 deletions fuocore/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import logging

from .utils import log_exectime

logger = logging.getLogger(__name__)


class Library(object):
"""library manages a set of providers
library is the entry point for music resources
Library is the entry point for music resources
"""
def __init__(self):
self._providers = set()
Expand Down Expand Up @@ -39,14 +41,11 @@ def search(self, keyword, source_in=None, **kwargs):
"""search song/artist/album by keyword
TODO: search album or artist
TODO: return a generator?
"""
for provider in self._providers:
if source_in is not None:
if provider.identifier not in source_in:
continue
if not provider.search:
continue

try:
result = provider.search(keyword=keyword)
Expand All @@ -55,3 +54,56 @@ def search(self, keyword, source_in=None, **kwargs):
logger.error('Search %s in %s failed.' % (keyword, provider))
else:
yield result

@log_exectime
def list_song_standby(self, song, onlyone=True):
"""try to list all valid standby
Search a song in all providers. The typical usage scenario is when a
song is not available in one provider, we can try to acquire it from other
providers.
Standby choosing strategy: search from all providers, select two song from each provide.
Those standby song should have same title and artist name.
TODO: maybe we should read a strategy from user config, user
knows which provider owns copyright about an artist.
FIXME: this method will send several network requests,
which may block the caller.
:param song: song model
:param exclude: exclude providers list
:return: list of songs (maximum count: 2)
"""
def get_score(standby):
score = 1
if song.artists_name != standby.artists_name:
score -= 0.3
if song.title != standby.title:
score -= 0.2
if song.album_name != standby.album_name:
score -= 0.1
return score

valid_sources = [p.identifier for p in self.list() if p.identifier != song.source]
q = '{} {}'.format(song.title, song.artists_name)

standby_list = []
for result in self.search(q, source_in=valid_sources, limit=10):
for standby in result.songs[:2]:
standby_list.append(standby)
standby_list = sorted(
standby_list,
key=lambda standby: get_score(standby), reverse=True
)

valid_standby_list = []
for standby in standby_list:
if standby.url:
valid_standby_list.append(standby)
if get_score(standby) == 1 or onlyone:
break
if len(valid_standby_list) >= 2:
break
return valid_standby_list
55 changes: 50 additions & 5 deletions fuocore/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,14 @@ def __new__(cls, name, bases, attrs):
model_name = _TYPE_NAME_MAP[ModelType(model_type)]
setattr(provider, model_name, klass)
fields = list(set(fields))

# DEPRECATED attribute _meta
klass._meta = ModelMetadata(model_type=model_type,
provider=provider,
fields=fields,
**meta_kv)
# use meta attribute instead of _meta
klass.meta = klass._meta
return klass


Expand Down Expand Up @@ -126,11 +130,13 @@ class BaseModel(Model):
:param identifier: model object identifier, unique in each provider
:param source: model object provider identifier
:cvar allow_get: whether model has a valid get method
:cvar allow_list: whether model has a valid list method
:cvar allow_get: meta var, whether model has a valid get method
:cvar allow_list: meta var, whether model has a valid list method
"""

class Meta:
allow_get = True
allow_list = False
model_type = ModelType.dummy.value
fields = ['source', 'identifier']

Expand Down Expand Up @@ -221,7 +227,7 @@ class Meta:
model_type = ModelType.song.value
# TODO: 支持低/中/高不同质量的音乐文件
fields = ['album', 'artists', 'lyric', 'comments', 'title', 'url',
'duration', ]
'duration',]

@property
def artists_name(self):
Expand Down Expand Up @@ -258,11 +264,11 @@ class Meta:
def __str__(self):
return 'fuo://{}/playlists/{}'.format(self.source, self.identifier)

def add_song(self, song_id, allow_exist=True):
def add(self, song_id, allow_exist=True):
"""add song to playlist"""
pass

def remove_song(self, song_id, allow_not_exist=True):
def remove(self, song_id, allow_not_exist=True):
"""remove songs from playlist"""
pass

Expand Down Expand Up @@ -297,6 +303,45 @@ class UserModel(BaseModel):
: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', 'playlists', 'fav_playlists', 'fav_songs',
'fav_albums', 'fav_artists']

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

def remove_from_fav_songs(self, song_id):
pass

def add_to_fav_playlists(self, playlist_id):
pass

def remove_from_fav_playlists(self, playlist_id):
pass

def add_to_fav_albums(self, album_id):
pass

def remove_from_fav_albums(self, album_id):
pass

def add_to_fav_artist(self, aritst_id):
pass

def remove_from_fav_artists(self, artist_id):
pass
52 changes: 0 additions & 52 deletions fuocore/netease/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,6 @@
logger = logging.getLogger(__name__)


class Xiami(object):
'''
refrence: https://github.com/listen1/listen1
'''
def __init__(self):
self._headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip,deflate,sdch',
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'api.xiami.com',
'Referer': 'http://m.xiami.com/',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)'
' AppleWebKit/537.36 (KHTML, like Gecko) Chrome'
'/33.0.1750.152 Safari/537.36',
}
pass

def search(self, keyword):
search_url = 'http://api.xiami.com/web?v=2.0&app_key=1&key={0}'\
'&page=1&limit=50&_ksTS=1459930568781_153&callback=jsonp154'\
'&r=search/songs'.format(keyword)
try:
res = requests.get(search_url, headers=self._headers)
json_string = res.content[9:-1]
data = json.loads(json_string.decode('utf-8'))
return data['data'].get('songs')
except Exception as e:
logger.error(str(e))
return []


class API(object):
def __init__(self):
super().__init__()
Expand All @@ -68,7 +35,6 @@ def __init__(self):
}
self._cookies = dict(appver="1.2.1", os="osx")
self._http = None
self.xiami_assister = Xiami()

@property
def cookies(self):
Expand Down Expand Up @@ -436,23 +402,5 @@ def encrypt_request(self, data):
}
return payload

def get_xiami_song(self, title, artist_name):
songs = self.xiami_assister.search(title)
if not songs:
return None

target_song = songs[0] # respect xiami search result
max_match_ratio = 0.5
for song in songs:
if song['song_name'].lower() == title.lower():
if song['artist_name'] == artist_name:
target_song = song
break
ratio = SequenceMatcher(None, song['artist_name'], artist_name).ratio()
if ratio > max_match_ratio:
max_match_ratio = ratio
target_song = song
return target_song['listen_file']


api = API()
11 changes: 1 addition & 10 deletions fuocore/netease/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,7 @@ def _refresh_url(self):
if songs and songs[0]['url']:
self.url = songs[0]['url']
else:
self.url = self._find_in_xiami() or ''

def _find_in_xiami(self):
logger.debug('try to find {} equivalent in xiami'.format(self))
return self._api.get_xiami_song(
title=self.title,
artist_name=self.artists_name
)
self.url = ''

def _find_in_local(self):
# TODO: make this a API in SongModel
Expand All @@ -89,8 +82,6 @@ def url(self):
"""
We will always check if this song file exists in local library,
if true, we return the url of the local file.
If a song does not exists in netease library, we will *try* to
find a equivalent in xiami temporarily.
.. note::
Expand Down
7 changes: 6 additions & 1 deletion fuocore/qqmusic/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ def get_song_url(self, song_mid):
'from': 'myqq',
'channel': 10007100,
}
# FIXME: set timeout to 2s as the responseis time is quite long
response = requests.get(url, params=params, headers=self._headers,
timeout=self._timeout)
timeout=2)
soup = BeautifulSoup(response.content, 'html.parser')
media = soup.select('#h5audio_media')
if media:
Expand All @@ -79,9 +80,13 @@ def search(self, keyword, limit=20, page=1):
path = '/soso/fcgi-bin/client_search_cp'
url = api_base_url + path
params = {
# w,n,page are required parameters
'w': keyword,
'n': limit,
'page': page,

# positional parameters
'cr': 1, # copyright?
}
response = requests.get(url, params=params, timeout=self._timeout)
content = response.text[9:-1]
Expand Down
4 changes: 2 additions & 2 deletions fuocore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ def wrapper(*args, **kwargs):
t = time.process_time()
result = func(*args, **kwargs)
elapsed_time = time.process_time() - t
logger.debug('function %s executed time: %f ms'
% (func.__name__, elapsed_time * 1000))
logger.debug('function %s executed time: %f ms',
func.__name__, elapsed_time * 1000)
return result
return wrapper

Expand Down
Loading

0 comments on commit e16c59b

Please sign in to comment.