fuo-qqmusic 1.0.4__tar.gz → 1.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fuo-qqmusic might be problematic. Click here for more details.
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/PKG-INFO +12 -2
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/README.md +8 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/api.py +35 -4
- fuo_qqmusic-1.0.6/fuo_qqmusic/login.py +24 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/provider.py +84 -23
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/provider_ui.py +5 -32
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/schemas.py +63 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic.egg-info/PKG-INFO +13 -3
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic.egg-info/SOURCES.txt +3 -1
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/setup.py +1 -1
- fuo_qqmusic-1.0.6/tests/test_provider.py +26 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/__init__.py +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/assets/icon.svg +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/consts.py +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic/excs.py +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic.egg-info/dependency_links.txt +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic.egg-info/entry_points.txt +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic.egg-info/requires.txt +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/fuo_qqmusic.egg-info/top_level.txt +0 -0
- {fuo_qqmusic-1.0.4 → fuo_qqmusic-1.0.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fuo_qqmusic
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: feeluown qqmusic plugin
|
|
5
5
|
Home-page: https://github.com/feeluown/feeluown-qqmusic
|
|
6
6
|
Author: Cosven
|
|
@@ -13,3 +13,13 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Requires-Dist: feeluown>=4.1.3
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: marshmallow>=3.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: summary
|
|
@@ -20,6 +20,14 @@ pip3 install fuo-qqmusic
|
|
|
20
20
|
[操作示例](https://github.com/feeluown/feeluown-qqmusic/issues/6)。
|
|
21
21
|
|
|
22
22
|
## changelog
|
|
23
|
+
### 1.0.6 (2025-05-08)
|
|
24
|
+
- 支持每日推荐
|
|
25
|
+
- 支持自动登录
|
|
26
|
+
|
|
27
|
+
### 1.0.5 (2024-06-03)
|
|
28
|
+
- 修复搜索接口,支持搜索歌单、视频、专辑
|
|
29
|
+
- 支持提供歌单的播放次数
|
|
30
|
+
|
|
23
31
|
### 1.0.4 (2024-05-21)
|
|
24
32
|
- 歌手歌曲排序切换为”按热度排序”
|
|
25
33
|
- 修复推荐歌单接口
|
|
@@ -58,7 +58,7 @@ class API(object):
|
|
|
58
58
|
Please http capture request from (mobile) qqmusic mobile web page
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
|
-
def __init__(self, timeout=
|
|
61
|
+
def __init__(self, timeout=2):
|
|
62
62
|
# TODO: 暂时无脑统一一个 timeout
|
|
63
63
|
# 正确的应该是允许不同接口有不同的超时时间
|
|
64
64
|
self._timeout = timeout
|
|
@@ -124,10 +124,14 @@ class API(object):
|
|
|
124
124
|
# Other supported types: songlist, user, mv, qc, gedantip, zhida.
|
|
125
125
|
if type_ == 0:
|
|
126
126
|
key_ = 'song'
|
|
127
|
-
elif type_ ==
|
|
128
|
-
key_ = 'album'
|
|
129
|
-
elif type_ == 9:
|
|
127
|
+
elif type_ == 1:
|
|
130
128
|
key_ = 'singer'
|
|
129
|
+
elif type_ == 2:
|
|
130
|
+
key_ = 'album'
|
|
131
|
+
elif type_ == 3:
|
|
132
|
+
key_ = 'songlist' # playlist
|
|
133
|
+
elif type_ == 4:
|
|
134
|
+
key_ = 'mv' # video
|
|
131
135
|
else:
|
|
132
136
|
raise QQIOError('invalid search type_:%d', type_)
|
|
133
137
|
payload = {
|
|
@@ -514,6 +518,33 @@ class API(object):
|
|
|
514
518
|
js = self.rpc(payload)
|
|
515
519
|
return js['songlist']['data']['tracks']
|
|
516
520
|
|
|
521
|
+
def get_diss_info(self, dissid, offset=0, limit=50):
|
|
522
|
+
"""获取歌单的详情
|
|
523
|
+
|
|
524
|
+
这是一种特殊的歌单,它的内容是动态的,由官方生成。比如“百万收藏”。
|
|
525
|
+
|
|
526
|
+
:param dissid: int, 举个例子 211111 是百万收藏。
|
|
527
|
+
:return: 参考 fixtures/get_diss_info.json
|
|
528
|
+
"""
|
|
529
|
+
payload = {
|
|
530
|
+
'req': {
|
|
531
|
+
"module": "music.srfDissInfo.aiDissInfo",
|
|
532
|
+
"method":"uniform_get_Dissinfo",
|
|
533
|
+
"param": {
|
|
534
|
+
"disstid":dissid,
|
|
535
|
+
"userinfo":1, # 不懂啥意思
|
|
536
|
+
"tag":1, # 不懂啥意思
|
|
537
|
+
"orderlist":1,
|
|
538
|
+
"song_begin": offset,
|
|
539
|
+
"song_num": limit,
|
|
540
|
+
"onlysonglist":0,
|
|
541
|
+
"enc_host_uin":"" # 注:即使登录了,这个也是空
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
js = self.rpc(payload)
|
|
546
|
+
return js['req']['data']
|
|
547
|
+
|
|
517
548
|
def get_song_url(self, song_mid):
|
|
518
549
|
uin = self._uin
|
|
519
550
|
songvkey = str(random.random()).replace("0.", "")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from feeluown.consts import DATA_DIR
|
|
5
|
+
|
|
6
|
+
USER_INFO_FILE = DATA_DIR + '/qqmusic_user_info.json'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_cookies():
|
|
10
|
+
if os.path.exists(USER_INFO_FILE):
|
|
11
|
+
# if the file is broken, just raise error
|
|
12
|
+
with open(USER_INFO_FILE) as f:
|
|
13
|
+
return json.load(f).get('cookies', None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_cookies(user, cookies):
|
|
17
|
+
js = {
|
|
18
|
+
'identifier': user.identifier,
|
|
19
|
+
'name': user.name,
|
|
20
|
+
'cookies': cookies
|
|
21
|
+
}
|
|
22
|
+
with open(USER_INFO_FILE, 'w') as f:
|
|
23
|
+
json.dump(js, f, indent=2)
|
|
24
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import List, Optional, Protocol
|
|
2
|
+
from typing import List, Optional, Protocol, Tuple
|
|
3
3
|
from feeluown.excs import ModelNotFound
|
|
4
4
|
from feeluown.library import (
|
|
5
5
|
AbstractProvider,
|
|
@@ -18,6 +18,7 @@ from feeluown.library import (
|
|
|
18
18
|
SupportsVideoGet,
|
|
19
19
|
SupportsSongLyric,
|
|
20
20
|
SupportsAlbumGet,
|
|
21
|
+
SupportsAlbumSongsReader,
|
|
21
22
|
SupportsArtistGet,
|
|
22
23
|
SupportsPlaylistGet,
|
|
23
24
|
SupportsPlaylistSongsReader,
|
|
@@ -25,10 +26,13 @@ from feeluown.library import (
|
|
|
25
26
|
SimpleSearchResult,
|
|
26
27
|
SearchType,
|
|
27
28
|
ModelType,
|
|
29
|
+
UserModel,
|
|
28
30
|
)
|
|
29
31
|
from feeluown.media import Media, Quality
|
|
30
32
|
from feeluown.utils.reader import create_reader, SequentialReader
|
|
31
33
|
from .api import API
|
|
34
|
+
from .login import read_cookies
|
|
35
|
+
from .excs import QQIOError
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
logger = logging.getLogger(__name__)
|
|
@@ -48,6 +52,7 @@ class Supports(
|
|
|
48
52
|
SupportsPlaylistGet,
|
|
49
53
|
SupportsPlaylistSongsReader,
|
|
50
54
|
SupportsRecACollectionOfSongs,
|
|
55
|
+
SupportsAlbumSongsReader,
|
|
51
56
|
Protocol,
|
|
52
57
|
):
|
|
53
58
|
pass
|
|
@@ -77,6 +82,31 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
77
82
|
def name(self):
|
|
78
83
|
return "QQ 音乐"
|
|
79
84
|
|
|
85
|
+
def auto_login(self):
|
|
86
|
+
cookies = read_cookies()
|
|
87
|
+
user, err = self.try_get_user_from_cookies(cookies)
|
|
88
|
+
if user:
|
|
89
|
+
self.auth(user)
|
|
90
|
+
else:
|
|
91
|
+
logger.info(f'Auto login failed: {err}')
|
|
92
|
+
|
|
93
|
+
def try_get_user_from_cookies(self, cookies) -> Tuple[Optional[UserModel], str]:
|
|
94
|
+
if not cookies: # is None or empty
|
|
95
|
+
return None, 'empty cookies'
|
|
96
|
+
|
|
97
|
+
uin = provider.api.get_uin_from_cookies(cookies)
|
|
98
|
+
if uin is None:
|
|
99
|
+
return None, "can't extract user info from cookies"
|
|
100
|
+
|
|
101
|
+
provider.api.set_cookies(cookies)
|
|
102
|
+
# try to extract current user
|
|
103
|
+
try:
|
|
104
|
+
user = provider.user_get(uin)
|
|
105
|
+
except QQIOError:
|
|
106
|
+
provider.api.set_cookies(None)
|
|
107
|
+
return None, 'get user info with cookies failed, expired cookies?'
|
|
108
|
+
return user, ''
|
|
109
|
+
|
|
80
110
|
def use_model_v2(self, mtype):
|
|
81
111
|
return mtype in (
|
|
82
112
|
ModelType.song,
|
|
@@ -224,6 +254,10 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
224
254
|
album = _deserialize(data_album, QQAlbumSchema)
|
|
225
255
|
return album
|
|
226
256
|
|
|
257
|
+
def album_create_songs_rd(self, album):
|
|
258
|
+
album = self.album_get(album.identifier)
|
|
259
|
+
return create_reader(album.songs)
|
|
260
|
+
|
|
227
261
|
def user_get(self, identifier):
|
|
228
262
|
data = self.api.user_detail(identifier)
|
|
229
263
|
data["creator"]["fav_pid"] = data["mymusic"][0]["id"]
|
|
@@ -253,6 +287,27 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
253
287
|
pl["logo"] = pl["cover"]
|
|
254
288
|
return [_deserialize(playlist, QQPlaylistSchema) for playlist in playlists]
|
|
255
289
|
|
|
290
|
+
def rec_list_daily_songs(self):
|
|
291
|
+
# TODO: cache API result
|
|
292
|
+
feed = self.api.get_recommend_feed()
|
|
293
|
+
card = None
|
|
294
|
+
for shelf_ in feed['v_shelf']:
|
|
295
|
+
if 'moduleID' not in shelf_['extra_info']:
|
|
296
|
+
for batch in shelf_['v_niche']:
|
|
297
|
+
for card in batch['v_card']:
|
|
298
|
+
if (
|
|
299
|
+
card['extra_info'].get('moduleID', '').startswith('recforyou')
|
|
300
|
+
and card['jumptype'] == 10014 # 10014->playlist
|
|
301
|
+
):
|
|
302
|
+
card = card
|
|
303
|
+
break
|
|
304
|
+
if card is None:
|
|
305
|
+
logger.warning("No daily songs found")
|
|
306
|
+
return []
|
|
307
|
+
playlist_id = card['id']
|
|
308
|
+
playlist = self.playlist_get(playlist_id)
|
|
309
|
+
return self.playlist_create_songs_rd(playlist).readall()
|
|
310
|
+
|
|
256
311
|
def rec_list_daily_playlists(self):
|
|
257
312
|
# TODO: cache API result
|
|
258
313
|
feed = self.api.get_recommend_feed()
|
|
@@ -267,14 +322,14 @@ class QQProvider(AbstractProvider, ProviderV2):
|
|
|
267
322
|
playlists = []
|
|
268
323
|
for batch in shelf['v_niche']:
|
|
269
324
|
for card in batch['v_card']:
|
|
270
|
-
print(card['title'], card['jumptype'])
|
|
271
325
|
if card['jumptype'] == 10014: # 10014->playlist
|
|
272
326
|
playlists.append(
|
|
273
327
|
PlaylistModel(identifier=str(card['id']),
|
|
274
328
|
source=SOURCE,
|
|
275
329
|
name=card['title'],
|
|
276
330
|
cover=card['cover'],
|
|
277
|
-
description=card['miscellany']['rcmdtemplate']
|
|
331
|
+
description=card['miscellany']['rcmdtemplate'],
|
|
332
|
+
play_count=card['cnt'])
|
|
278
333
|
)
|
|
279
334
|
return playlists
|
|
280
335
|
|
|
@@ -394,26 +449,29 @@ def create_g(func, identifier, schema):
|
|
|
394
449
|
|
|
395
450
|
def search(keyword, **kwargs):
|
|
396
451
|
type_ = SearchType.parse(kwargs["type_"])
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
452
|
+
type_type_map = {
|
|
453
|
+
SearchType.so: 0,
|
|
454
|
+
SearchType.ar: 1,
|
|
455
|
+
SearchType.al: 2,
|
|
456
|
+
SearchType.pl: 3,
|
|
457
|
+
SearchType.vi: 4,
|
|
458
|
+
}
|
|
459
|
+
data = provider.api.search(keyword, type_=type_type_map[type_])
|
|
460
|
+
if type_ == SearchType.so:
|
|
461
|
+
songs = [_deserialize(song, QQSongSchema) for song in data]
|
|
462
|
+
return SimpleSearchResult(q=keyword, songs=songs)
|
|
463
|
+
if type_ == SearchType.ar:
|
|
464
|
+
artists = [_deserialize(artist, SearchArtistSchema) for artist in data]
|
|
465
|
+
return SimpleSearchResult(q=keyword, artists=artists)
|
|
466
|
+
elif type_ == SearchType.al:
|
|
467
|
+
albums = [_deserialize(album, SearchAlbumSchema) for album in data]
|
|
468
|
+
return SimpleSearchResult(q=keyword, albums=albums)
|
|
469
|
+
elif type_ == SearchType.pl:
|
|
470
|
+
playlists = [_deserialize(playlist, SearchPlaylistSchema) for playlist in data]
|
|
400
471
|
return SimpleSearchResult(q=keyword, playlists=playlists)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
SearchType.al: 8,
|
|
405
|
-
SearchType.ar: 9,
|
|
406
|
-
}
|
|
407
|
-
data = provider.api.search(keyword, type_=type_type_map[type_])
|
|
408
|
-
if type_ == SearchType.so:
|
|
409
|
-
songs = [_deserialize(song, QQSongSchema) for song in data]
|
|
410
|
-
return SimpleSearchResult(q=keyword, songs=songs)
|
|
411
|
-
elif type_ == SearchType.al:
|
|
412
|
-
albums = [_deserialize(album, _BriefAlbumSchema) for album in data]
|
|
413
|
-
return SimpleSearchResult(q=keyword, albums=albums)
|
|
414
|
-
else:
|
|
415
|
-
artists = [_deserialize(artist, _BriefArtistSchema) for artist in data]
|
|
416
|
-
return SimpleSearchResult(q=keyword, artists=artists)
|
|
472
|
+
elif type_ == SearchType.vi:
|
|
473
|
+
models = [_deserialize(model, SearchMVSchema) for model in data]
|
|
474
|
+
return SimpleSearchResult(q=keyword, videos=models)
|
|
417
475
|
|
|
418
476
|
|
|
419
477
|
provider = QQProvider()
|
|
@@ -427,9 +485,12 @@ from .schemas import ( # noqa
|
|
|
427
485
|
_BriefAlbumSchema,
|
|
428
486
|
_UserArtistSchema,
|
|
429
487
|
_BriefArtistSchema,
|
|
430
|
-
_BriefPlaylistSchema,
|
|
431
488
|
QQAlbumSchema,
|
|
432
489
|
QQPlaylistSchema,
|
|
433
490
|
QQUserSchema,
|
|
434
491
|
_UserAlbumSchema,
|
|
492
|
+
SearchAlbumSchema,
|
|
493
|
+
SearchArtistSchema,
|
|
494
|
+
SearchPlaylistSchema,
|
|
495
|
+
SearchMVSchema,
|
|
435
496
|
) # noqa
|
|
@@ -11,20 +11,11 @@ from feeluown.app.gui_app import GuiApp
|
|
|
11
11
|
|
|
12
12
|
from .provider import provider
|
|
13
13
|
from .excs import QQIOError
|
|
14
|
+
from .login import read_cookies, write_cookies
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
USER_INFO_FILE = DATA_DIR + '/qqmusic_user_info.json'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def read_cookies():
|
|
22
|
-
if os.path.exists(USER_INFO_FILE):
|
|
23
|
-
# if the file is broken, just raise error
|
|
24
|
-
with open(USER_INFO_FILE) as f:
|
|
25
|
-
return json.load(f).get('cookies', None)
|
|
26
|
-
|
|
27
|
-
|
|
28
19
|
class ProviderUI(AbstractProviderUi):
|
|
29
20
|
def __init__(self, app: GuiApp):
|
|
30
21
|
self._app = app
|
|
@@ -72,31 +63,13 @@ class LoginDialog(CookiesLoginDialog):
|
|
|
72
63
|
provider._user = user
|
|
73
64
|
|
|
74
65
|
async def user_from_cookies(self, cookies):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
uin = provider.api.get_uin_from_cookies(cookies)
|
|
79
|
-
if uin is None:
|
|
80
|
-
raise InvalidCookies("can't extract user info from cookies")
|
|
81
|
-
|
|
82
|
-
provider.api.set_cookies(cookies)
|
|
83
|
-
# try to extract current user
|
|
84
|
-
try:
|
|
85
|
-
user = await run_fn(provider.user_get, uin)
|
|
86
|
-
except QQIOError:
|
|
87
|
-
provider.api.set_cookies(None)
|
|
88
|
-
raise InvalidCookies('get user info with cookies failed, expired cookies?')
|
|
89
|
-
else:
|
|
66
|
+
user, err = await run_fn(provider.try_get_user_from_cookies, cookies)
|
|
67
|
+
if user:
|
|
90
68
|
return user
|
|
69
|
+
raise InvalidCookies(err)
|
|
91
70
|
|
|
92
71
|
def load_user_cookies(self):
|
|
93
72
|
return read_cookies()
|
|
94
73
|
|
|
95
74
|
def dump_user_cookies(self, user, cookies):
|
|
96
|
-
|
|
97
|
-
'identifier': user.identifier,
|
|
98
|
-
'name': user.name,
|
|
99
|
-
'cookies': cookies
|
|
100
|
-
}
|
|
101
|
-
with open(USER_INFO_FILE, 'w') as f:
|
|
102
|
-
json.dump(js, f, indent=2)
|
|
75
|
+
write_cookies(user, cookies)
|
|
@@ -10,6 +10,7 @@ from feeluown.library import (
|
|
|
10
10
|
PlaylistModel,
|
|
11
11
|
BriefPlaylistModel,
|
|
12
12
|
UserModel,
|
|
13
|
+
VideoModel,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -143,6 +144,17 @@ class _BriefArtistSchema(Schema):
|
|
|
143
144
|
return create_model(BriefArtistModel, data, ["mid"])
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
class SearchArtistSchema(_BriefArtistSchema):
|
|
148
|
+
pic_url = fields.Str(data_key="singerPic", required=True)
|
|
149
|
+
|
|
150
|
+
@post_load
|
|
151
|
+
def create_model(self, data, **kwargs):
|
|
152
|
+
data['hot_songs'] = []
|
|
153
|
+
data['description'] = ''
|
|
154
|
+
data['aliases'] = []
|
|
155
|
+
return create_model(ArtistModel, data, ["mid"])
|
|
156
|
+
|
|
157
|
+
|
|
146
158
|
class _BriefAlbumSchema(Schema):
|
|
147
159
|
identifier = fields.Int(data_key="albumID", required=True)
|
|
148
160
|
mid = fields.Str(data_key="albumMID", required=True)
|
|
@@ -153,6 +165,20 @@ class _BriefAlbumSchema(Schema):
|
|
|
153
165
|
return create_model(BriefAlbumModel, data, ["mid"])
|
|
154
166
|
|
|
155
167
|
|
|
168
|
+
class SearchAlbumSchema(_BriefAlbumSchema):
|
|
169
|
+
cover = fields.Str(data_key="albumPic", required=True)
|
|
170
|
+
released = fields.Str(data_key="publicTime", required=True)
|
|
171
|
+
song_count = fields.Int(required=True)
|
|
172
|
+
artists = fields.List(fields.Nested(_SongArtistSchema),
|
|
173
|
+
data_key="singer_list",
|
|
174
|
+
required=True)
|
|
175
|
+
@post_load
|
|
176
|
+
def create_model(self, data, **kwargs):
|
|
177
|
+
data['description'] = ''
|
|
178
|
+
data['songs'] = []
|
|
179
|
+
return create_model(AlbumModel, data, ['mid'])
|
|
180
|
+
|
|
181
|
+
|
|
156
182
|
class _BriefPlaylistSchema(Schema):
|
|
157
183
|
identifier = fields.Int(data_key="dissid", required=True)
|
|
158
184
|
name = fields.Str(data_key="dissname", required=True)
|
|
@@ -164,6 +190,27 @@ class _BriefPlaylistSchema(Schema):
|
|
|
164
190
|
return create_model(PlaylistModel, **data)
|
|
165
191
|
|
|
166
192
|
|
|
193
|
+
class PlaylistUserSchema(Schema):
|
|
194
|
+
identifier = fields.Str(data_key="creator_uin", required=True)
|
|
195
|
+
mid = fields.Str(data_key="encrypt_uin", required=True)
|
|
196
|
+
name = fields.Str(required=True)
|
|
197
|
+
avatar_url = fields.Str(required=True, data_key="avatarUrl")
|
|
198
|
+
|
|
199
|
+
@post_load
|
|
200
|
+
def create_model(self, data, **kwargs):
|
|
201
|
+
return create_model(UserModel, data, ['mid'])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class SearchPlaylistSchema(_BriefPlaylistSchema):
|
|
205
|
+
creator = fields.Nested("PlaylistUserSchema", required=True)
|
|
206
|
+
description = fields.Str(data_key="introduction", required=True)
|
|
207
|
+
play_count = fields.Int(data_key="listennum", required=True)
|
|
208
|
+
|
|
209
|
+
@post_load
|
|
210
|
+
def create_model(self, data, **kwargs):
|
|
211
|
+
return create_model(PlaylistModel, data)
|
|
212
|
+
|
|
213
|
+
|
|
167
214
|
class QQArtistSchema(Schema):
|
|
168
215
|
"""歌手详情 Schema、歌曲歌手简要信息 Schema"""
|
|
169
216
|
|
|
@@ -294,3 +341,19 @@ class QQUserSchema(Schema):
|
|
|
294
341
|
playlists=playlists,
|
|
295
342
|
)
|
|
296
343
|
return create_model(UserModel, data, ['mid', 'fav_pid', 'playlists'])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class SearchMVSchema(Schema):
|
|
347
|
+
# 使用 mv_id 字段的话,目前拿不到播放 url,用 v_id 比较合适
|
|
348
|
+
identifier = fields.Str(data_key="v_id", required=True)
|
|
349
|
+
title = fields.Str(data_key="mv_name", required=True)
|
|
350
|
+
artists = fields.List(fields.Nested("_SongArtistSchema"),
|
|
351
|
+
data_key="singer_list",
|
|
352
|
+
required=True)
|
|
353
|
+
duration = fields.Int(required=True)
|
|
354
|
+
cover = fields.Str(data_key="mv_pic_url", required=True)
|
|
355
|
+
play_count = fields.Int(required=True)
|
|
356
|
+
|
|
357
|
+
@post_load
|
|
358
|
+
def create_model(self, data, **kwargs):
|
|
359
|
+
return create_model(VideoModel, data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 1.0.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fuo_qqmusic
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: feeluown qqmusic plugin
|
|
5
5
|
Home-page: https://github.com/feeluown/feeluown-qqmusic
|
|
6
6
|
Author: Cosven
|
|
@@ -13,3 +13,13 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Requires-Dist: feeluown>=4.1.3
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: marshmallow>=3.0
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: summary
|
|
@@ -4,6 +4,7 @@ fuo_qqmusic/__init__.py
|
|
|
4
4
|
fuo_qqmusic/api.py
|
|
5
5
|
fuo_qqmusic/consts.py
|
|
6
6
|
fuo_qqmusic/excs.py
|
|
7
|
+
fuo_qqmusic/login.py
|
|
7
8
|
fuo_qqmusic/provider.py
|
|
8
9
|
fuo_qqmusic/provider_ui.py
|
|
9
10
|
fuo_qqmusic/schemas.py
|
|
@@ -13,4 +14,5 @@ fuo_qqmusic.egg-info/dependency_links.txt
|
|
|
13
14
|
fuo_qqmusic.egg-info/entry_points.txt
|
|
14
15
|
fuo_qqmusic.egg-info/requires.txt
|
|
15
16
|
fuo_qqmusic.egg-info/top_level.txt
|
|
16
|
-
fuo_qqmusic/assets/icon.svg
|
|
17
|
+
fuo_qqmusic/assets/icon.svg
|
|
18
|
+
tests/test_provider.py
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from fuo_qqmusic import provider
|
|
8
|
+
from fuo_qqmusic.api import API
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_json_fixture(path):
|
|
12
|
+
path = os.path.join('data/fixtures', path)
|
|
13
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
14
|
+
data = json.load(f)
|
|
15
|
+
return data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def album_3913679():
|
|
20
|
+
return _read_json_fixture('album_3913679.json')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_provider_album_get(album_3913679):
|
|
24
|
+
patch.object(API, 'album_detail', return_value=album_3913679)
|
|
25
|
+
album = provider.album_get('3913679')
|
|
26
|
+
assert album.identifier == '3913679'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|