fuo-qqmusic 0.5.0__tar.gz → 1.0__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.

Files changed (25) hide show
  1. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/PKG-INFO +1 -1
  2. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/README.md +7 -7
  3. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/__init__.py +4 -4
  4. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/api.py +105 -27
  5. fuo_qqmusic-1.0/fuo_qqmusic/provider.py +371 -0
  6. fuo_qqmusic-1.0/fuo_qqmusic/provider_ui.py +102 -0
  7. fuo_qqmusic-1.0/fuo_qqmusic/schemas.py +296 -0
  8. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/PKG-INFO +1 -1
  9. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/SOURCES.txt +1 -5
  10. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/setup.py +1 -1
  11. fuo_qqmusic-0.5.0/fuo_qqmusic/models.py +0 -355
  12. fuo_qqmusic-0.5.0/fuo_qqmusic/page_daily_recommendation.py +0 -23
  13. fuo_qqmusic-0.5.0/fuo_qqmusic/page_explore.py +0 -81
  14. fuo_qqmusic-0.5.0/fuo_qqmusic/page_fav.py +0 -55
  15. fuo_qqmusic-0.5.0/fuo_qqmusic/provider.py +0 -47
  16. fuo_qqmusic-0.5.0/fuo_qqmusic/schemas.py +0 -242
  17. fuo_qqmusic-0.5.0/fuo_qqmusic/ui.py +0 -133
  18. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/assets/icon.svg +0 -0
  19. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/consts.py +0 -0
  20. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/excs.py +0 -0
  21. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/dependency_links.txt +0 -0
  22. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/entry_points.txt +0 -0
  23. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/requires.txt +0 -0
  24. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/top_level.txt +0 -0
  25. {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fuo_qqmusic
3
- Version: 0.5.0
3
+ Version: 1.0
4
4
  Summary: feeluown qqmusic plugin
5
5
  Home-page: https://github.com/feeluown/feeluown-qqmusic
6
6
  Author: Cosven
@@ -21,41 +21,41 @@ pip3 install fuo-qqmusic
21
21
 
22
22
  ## changelog
23
23
 
24
- ### 0.5.0 (2023-07-14)
24
+ ### 1.0 (2024-01-1)
25
+ - 使用 FeelUOwn 新接口
26
+ - 支持发现,推荐等接口
27
+
28
+ ### 0.5.1 (2023-08-14)
29
+ - 恢复部分接口(0.5.0 移除了一些实际上可用的接口)
25
30
 
31
+ ### 0.5.0 (2023-07-14)
26
32
  - 修复搜索接口
27
33
  - 让部分不可用接口直接报错
28
34
 
29
35
  ### 0.4.1 (2022-03-21)
30
-
31
36
  - 修复加载歌单时,应用可能会 crash 的问题
32
37
 
33
38
  ### 0.4.0 (???)
34
-
35
39
  - 修复若干问题
36
40
 
37
41
  ### 0.3.4 (2022-01-31)
38
-
39
42
  感谢 [@cyliuu](https://github.com/cyliuu)
40
43
 
41
44
  - 修复若干问题
42
45
 
43
46
  ### 0.3.3 (2021-06-10)
44
-
45
47
  感谢 [@cyliuu](https://github.com/cyliuu)
46
48
 
47
49
  - 支持展示收藏的歌曲、专辑和歌手
48
50
  - 支持获取相似歌曲
49
51
 
50
52
  ### 0.3.2 (2021-04-23)
51
-
52
53
  感谢 [@cyliuu](https://github.com/cyliuu)
53
54
 
54
55
  - 提供更准确的音乐的比特率
55
56
  - 绿钻用户使用合适的 cookie 可以获取到高品质音乐
56
57
 
57
58
  ### 0.3 (2020-08-10)
58
-
59
59
  感谢 [@csy19960309](https://github.com/csy19960309)
60
60
 
61
61
  - 支持登陆功能,加载用户歌单
@@ -8,15 +8,15 @@ __version__ = '0.3a0'
8
8
  __desc__ = 'QQ 音乐'
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
- ui_mgr = None
12
11
 
13
12
 
14
13
  def enable(app):
15
- global ui_mgr
16
14
  app.library.register(provider)
17
15
  if app.mode & app.GuiMode:
18
- from .ui import UiManager
19
- ui_mgr = ui_mgr or UiManager(app)
16
+ from .provider_ui import ProviderUI
17
+
18
+ provider_ui = ProviderUI(app)
19
+ app.pvd_ui_mgr.register(provider_ui)
20
20
 
21
21
 
22
22
  def disable(app):
@@ -2,7 +2,6 @@
2
2
  # encoding: UTF-8
3
3
 
4
4
  import base64
5
- import re
6
5
  import hashlib
7
6
  import logging
8
7
  import math
@@ -15,6 +14,8 @@ from .excs import QQIOError
15
14
 
16
15
  logger = logging.getLogger(__name__)
17
16
 
17
+ api_base_url = 'http://c.y.qq.com'
18
+
18
19
 
19
20
  def djb2(string):
20
21
  ''' Hash a word using the djb2 algorithm with the specified base. '''
@@ -240,16 +241,62 @@ class API(object):
240
241
  return js['req_0']['data']
241
242
 
242
243
  def artist_albums(self, artist_id, page=1, page_size=20):
243
- raise QQIOError('artist_albums api is not available')
244
+ url = api_base_url + '/v8/fcg-bin/fcg_v8_singer_album.fcg'
245
+ params = {
246
+ 'singerid': artist_id,
247
+ 'order': 'time',
248
+ 'begin': (page - 1) * page_size, # TODO: 这里应该代表偏移量
249
+ 'num': page_size
250
+ }
251
+ response = requests.get(url, params=params)
252
+ js = response.json()
253
+ return js['data']
244
254
 
245
255
  def album_detail(self, album_id):
246
- raise QQIOError('album_detail api is not available')
256
+ url = api_base_url + '/v8/fcg-bin/fcg_v8_album_detail_cp.fcg'
257
+ params = {
258
+ 'albumid': album_id,
259
+ 'format': 'json',
260
+ 'newsong': 1
261
+ }
262
+ resp = requests.get(url, params=params)
263
+ return resp.json()['data']
247
264
 
248
265
  def playlist_detail(self, pid, offset=0, limit=50):
249
- raise QQIOError('playlist_detail api is not available')
266
+ url = api_base_url + '/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg'
267
+ params = {
268
+ 'type': '1',
269
+ 'utf8': '1',
270
+ 'disstid': pid,
271
+ 'format': 'json',
272
+ 'new_format': '1', # 需要这个字段来获取file等信息
273
+ 'song_begin': offset,
274
+ 'song_num': limit,
275
+ }
276
+ resp = requests.get(url, params=params, headers=self._headers,
277
+ cookies=self._cookies, timeout=self._timeout)
278
+ js = resp.json()
279
+ if js['code'] != 0:
280
+ raise CodeShouldBe0(js)
281
+ return js['cdlist'][0]
250
282
 
251
283
  def user_detail(self, uid):
252
- raise QQIOError('user_detail api is not available')
284
+ """
285
+ this API can be called only when user has logged in
286
+ """
287
+ url = api_base_url + '/rsc/fcgi-bin/fcg_get_profile_homepage.fcg'
288
+ params = {
289
+ # 这两个字段意义不明,不过至少固定为此值时可正常使用
290
+ 'cid': 205360838,
291
+ 'reqfrom': 1,
292
+ 'userid': uid
293
+ }
294
+ resp = requests.get(url, params=params, headers=self._headers,
295
+ cookies=self._cookies, timeout=self._timeout)
296
+ js = resp.json()
297
+ if js['code'] != 0:
298
+ raise CodeShouldBe0(js)
299
+ return js['data']
253
300
 
254
301
  def user_favorite_artists(self, uid, mid, page=1, page_size=30):
255
302
  # FIXME: page/page_size is just a guess
@@ -263,37 +310,57 @@ class API(object):
263
310
  'HostUin': mid
264
311
  }},
265
312
  'comm': {
266
- 'g_tk': self.get_token_from_cookies(),
313
+ "cv": 4747474,
314
+ "ct": 24,
315
+ 'g_tk': int(self.get_token_from_cookies()),
316
+ 'g_tk_new_20200303': int(self.get_token_from_cookies()),
267
317
  'uin': uid,
268
318
  'format': 'json',
319
+ 'notice': 0,
320
+ 'platform': 'yqq.json',
321
+ 'needNewCode': 1,
269
322
  }
270
323
  }
271
324
  js = self.rpc(payload)
272
- return js['req_0']['data']['List']
325
+ return js['req_1']['data']['List']
273
326
 
274
327
  def user_favorite_albums(self, uid, start=0, end=100):
275
- raise QQIOError('user_detail api is not available')
328
+ url = api_base_url + '/fav/fcgi-bin/fcg_get_profile_order_asset.fcg'
329
+ params = {
330
+ 'ct': 20, # 不知道此字段什么含义
331
+ 'reqtype': 2,
332
+ 'sin': start, # 每一页的开始
333
+ 'ein': end, # 每一页的结尾,目前假设最多收藏 30 个专辑
334
+ 'cid': 205360956,
335
+ 'reqfrom': 1,
336
+ 'userid': uid
337
+ }
338
+ resp = requests.get(url, params=params, headers=self._headers,
339
+ cookies=self._cookies, timeout=self._timeout)
340
+ js = resp.json()
341
+ if js['code'] != 0:
342
+ raise CodeShouldBe0(js)
343
+ return js['data']['albumlist']
276
344
 
277
345
  def user_favorite_playlists(self, uid, mid, start=0, end=100):
278
- raise QQIOError('api is not available')
346
+ url = api_base_url + '/fav/fcgi-bin/fcg_get_profile_order_asset.fcg'
279
347
 
280
- def get_recommend_songs_pid(self):
281
- data = {
282
- 'req_0': {
283
- 'module': 'recommend.RecommendFeedServer',
284
- 'method': 'get_recommend_feed',
285
- 'param': {
286
- 'direction': 0,
287
- 'page': 1,
288
- 'v_cache': [],
289
- 'v_uniq': [],
290
- 's_num': 0
291
- }
292
- },
348
+ params = {
349
+ 'loginUin': uid,
350
+ 'userid': mid,
351
+ 'cid': 205360956,
352
+ 'sin': start,
353
+ 'ein': end,
354
+ 'reqtype': 3,
355
+ 'ct': 20, # 没有该字段 返回中文字符是乱码
293
356
  }
294
- js = self.rpc(data)
295
- disstid = js['req_0']['data']['v_shelf'][0]['v_niche'][0]['v_card'][1]['id']
296
- return disstid
357
+
358
+ resp = requests.get(url, params=params, headers=self._headers,
359
+ cookies=self._cookies, timeout=self._timeout)
360
+ js = resp.json()
361
+ if js['code'] != 0:
362
+ raise CodeShouldBe0(js)
363
+ return js['data']['cdlist']
297
364
 
298
365
  def recommend_playlists(self):
299
366
  data = {
@@ -330,7 +397,18 @@ class API(object):
330
397
  return ids
331
398
 
332
399
  def get_lyric_by_songmid(self, songmid):
333
- raise QQIOError('get_lyric_by_songmid is not available')
400
+ url = api_base_url + '/lyric/fcgi-bin/fcg_query_lyric_new.fcg'
401
+ params = {
402
+ 'songmid': songmid,
403
+ 'pcachetime': int(round(time.time() * 1000)),
404
+ 'format': 'json',
405
+ }
406
+ response = requests.get(url, params=params, headers=self._headers,
407
+ timeout=self._timeout)
408
+ js = response.json()
409
+ CodeShouldBe0.check(js)
410
+ lyric = js['lyric'] or ''
411
+ return base64.b64decode(lyric).decode()
334
412
 
335
413
  def rpc(self, payload):
336
414
  if 'comm' not in payload:
@@ -343,7 +421,7 @@ class API(object):
343
421
  }
344
422
  url = 'https://u.y.qq.com/cgi-bin/musicu.fcg'
345
423
  resp = requests.get(url, params=params, headers=self._headers,
346
- timeout=self._timeout)
424
+ cookies=self._cookies, timeout=self._timeout)
347
425
  js = resp.json()
348
426
  CodeShouldBe0.check(js)
349
427
  return js
@@ -0,0 +1,371 @@
1
+ import logging
2
+ from typing import List, Optional, Protocol
3
+ from feeluown.excs import ModelNotFound
4
+ from feeluown.library import (
5
+ AbstractProvider,
6
+ ProviderV2,
7
+ ProviderFlags as PF,
8
+ SupportsSongGet,
9
+ SupportsSongMultiQuality,
10
+ SupportsSongMV,
11
+ VideoModel,
12
+ LyricModel,
13
+ SupportsCurrentUser,
14
+ SupportsVideoGet,
15
+ SupportsSongLyric,
16
+ SupportsAlbumGet,
17
+ SupportsArtistGet,
18
+ SupportsPlaylistGet,
19
+ SupportsPlaylistSongsReader,
20
+ SimpleSearchResult,
21
+ SearchType,
22
+ ModelType,
23
+ )
24
+ from feeluown.media import Media, Quality
25
+ from feeluown.utils.reader import create_reader, SequentialReader
26
+ from .api import API
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+ UNFETCHED_MEDIA = object()
31
+ SOURCE = "qqmusic"
32
+
33
+
34
+ class Supports(
35
+ SupportsSongGet,
36
+ SupportsSongMultiQuality,
37
+ SupportsSongMV,
38
+ SupportsCurrentUser,
39
+ SupportsVideoGet,
40
+ SupportsSongLyric,
41
+ SupportsAlbumGet,
42
+ SupportsArtistGet,
43
+ SupportsPlaylistGet,
44
+ SupportsPlaylistSongsReader,
45
+ Protocol,
46
+ ):
47
+ pass
48
+
49
+
50
+ class QQProvider(AbstractProvider, ProviderV2):
51
+ class meta:
52
+ identifier = "qqmusic"
53
+ name = "QQ 音乐"
54
+ flags = {
55
+ ModelType.song: PF.similar,
56
+ ModelType.none: PF.current_user,
57
+ }
58
+
59
+ def __init__(self):
60
+ super().__init__()
61
+ self.api = API()
62
+
63
+ def _(self) -> Supports:
64
+ return self
65
+
66
+ @property
67
+ def identifier(self):
68
+ return "qqmusic"
69
+
70
+ @property
71
+ def name(self):
72
+ return "QQ 音乐"
73
+
74
+ def use_model_v2(self, mtype):
75
+ return mtype in (
76
+ ModelType.song,
77
+ ModelType.album,
78
+ ModelType.artist,
79
+ ModelType.playlist,
80
+ )
81
+
82
+ def song_get(self, identifier):
83
+ data = self.api.song_detail(identifier)
84
+ return _deserialize(data, QQSongSchema)
85
+
86
+ def song_get_mv(self, song):
87
+ mv_id = self._model_cache_get_or_fetch(song, "mv_id")
88
+ if mv_id == 0:
89
+ return None
90
+ video = self.video_get(mv_id)
91
+ # mv 的名字是空的
92
+ video.title = song.title
93
+ return video
94
+
95
+ def song_get_lyric(self, song):
96
+ mid = self._model_cache_get_or_fetch(song, "mid")
97
+ content = self.api.get_lyric_by_songmid(mid)
98
+ return LyricModel(identifier=mid, source=SOURCE, content=content)
99
+
100
+ def video_get(self, identifier):
101
+ data = self.api.get_mv(identifier)
102
+ if not data:
103
+ raise ModelNotFound(f"mv:{identifier} not found")
104
+ fhd = hd = sd = ld = None
105
+ for file in data["mp4"]:
106
+ if not file["url"]:
107
+ continue
108
+ file_type = file["filetype"]
109
+ url = file["freeflow_url"][0]
110
+ if file_type == 40:
111
+ fhd = url
112
+ elif file_type == 30:
113
+ hd = url
114
+ elif file_type == 20:
115
+ sd = url
116
+ elif file_type == 10:
117
+ ld = url
118
+ elif file_type == 0:
119
+ pass
120
+ else:
121
+ logger.warning("There exists another quality:%s mv.", str(file_type))
122
+ q_url_mapping = dict(fhd=fhd, hd=hd, sd=sd, ld=ld)
123
+ video = VideoModel(
124
+ identifier=identifier,
125
+ source=SOURCE,
126
+ title="未知名字",
127
+ artists=[],
128
+ duration=1,
129
+ cover="",
130
+ )
131
+ video.cache_set("q_url_mapping", q_url_mapping)
132
+ return video
133
+
134
+ def video_get_media(self, video, quality):
135
+ q_media_mapping = self._model_cache_get_or_fetch(video, "q_url_mapping")
136
+ return Media(q_media_mapping[quality.value])
137
+
138
+ def video_list_quality(self, video):
139
+ q_media_mapping = self._model_cache_get_or_fetch(video, "q_url_mapping")
140
+ return [Quality.Video(k) for k, v in q_media_mapping.items() if v]
141
+
142
+ def song_list_quality(self, song) -> List[Quality.Audio]:
143
+ """List all possible qualities
144
+
145
+ Please ensure all the qualities are valid. `song_get_media(song, quality)`
146
+ must not return None with a valid quality.
147
+ """
148
+ return list(self._song_get_q_media_mapping(song))
149
+
150
+ def song_get_media(self, song, quality: Quality.Audio) -> Optional[Media]:
151
+ """Get song's media by a specified quality
152
+
153
+ :return: when quality is invalid, return None
154
+ """
155
+ q_media_mapping = self._song_get_q_media_mapping(song)
156
+ quality_suffix = song.cache_get("quality_suffix")
157
+ mid = song.cache_get("mid")
158
+ media_id = song.cache_get("media_id")
159
+ media = q_media_mapping.get(quality)
160
+ if media is UNFETCHED_MEDIA:
161
+ for q, t, b, s in quality_suffix:
162
+ if quality == q:
163
+ url = self.api.get_song_url_v2(mid, media_id, t)
164
+ if url:
165
+ media = Media(url, bitrate=b, format=s)
166
+ q_media_mapping[quality] = media
167
+ else:
168
+ media = None
169
+ q_media_mapping[quality] = None
170
+ break
171
+ else:
172
+ media = None
173
+ return media
174
+
175
+ def _song_get_q_media_mapping(self, song):
176
+ q_media_mapping, exists = song.cache_get("q_media_mapping")
177
+ if exists is True:
178
+ return q_media_mapping
179
+ quality_suffix = self._model_cache_get_or_fetch(song, "quality_suffix")
180
+ mid = self._model_cache_get_or_fetch(song, "mid")
181
+ media_id = self._model_cache_get_or_fetch(song, "media_id")
182
+ q_media_mapping = {}
183
+ # 注:self.quality_suffix 这里可能会触发一次网络请求
184
+ for idx, (q, t, b, s) in enumerate(quality_suffix):
185
+ url = self.api.get_song_url_v2(mid, media_id, t)
186
+ if url:
187
+ q_media_mapping[Quality.Audio(q)] = Media(url, bitrate=b, format=s)
188
+ # 一般来说,高品质有权限 低品质也会有权限,减少网络请求。
189
+ # 这里把值设置为 UNFETCHED_MEDIA,作为一个标记。
190
+ for i in range(idx + 1, len(quality_suffix)):
191
+ q_media_mapping[
192
+ Quality.Audio(quality_suffix[i][0])
193
+ ] = UNFETCHED_MEDIA
194
+ break
195
+ song.cache_set("q_media_mapping", q_media_mapping)
196
+ return q_media_mapping
197
+
198
+ def artist_get(self, identifier):
199
+ data_mid = self.api.artist_songs(int(identifier), 1, 0)["singerMid"]
200
+ data_artist = self.api.artist_detail(data_mid)
201
+ artist = _deserialize(data_artist, QQArtistSchema)
202
+ return artist
203
+
204
+ def artist_create_songs_rd(self, artist):
205
+ return create_g(
206
+ self.api.artist_songs, int(artist.identifier), _ArtistSongSchema
207
+ )
208
+
209
+ def artist_create_albums_rd(self, artist):
210
+ return create_g(
211
+ self.api.artist_albums, int(artist.identifier), _BriefAlbumSchema
212
+ )
213
+
214
+ def album_get(self, identifier):
215
+ data_album = self.api.album_detail(int(identifier))
216
+ if data_album is None:
217
+ raise ModelNotFound
218
+ album = _deserialize(data_album, QQAlbumSchema)
219
+ return album
220
+
221
+ def user_get(self, identifier):
222
+ data = self.api.user_detail(identifier)
223
+ data["creator"]["fav_pid"] = data["mymusic"][0]["id"]
224
+ # 假设使用微信登陆,从网页拿到 cookie,cookie 里面的 uin 是正确的,
225
+ # 而这个接口返回的 uin 则可能是 0,因此手动重置一下。
226
+ data["creator"]["uin"] = identifier
227
+ return _deserialize(data, QQUserSchema)
228
+
229
+ def playlist_get(self, identifier):
230
+ data = self.api.playlist_detail(int(identifier), limit=1000)
231
+ return _deserialize(data, QQPlaylistSchema)
232
+
233
+ def playlist_create_songs_rd(self, playlist):
234
+ songs = self._model_cache_get_or_fetch(playlist, "songs")
235
+ return create_reader(songs)
236
+
237
+ def rec_list_daily_playlists(self):
238
+ user = self.get_current_user()
239
+ if user is None:
240
+ return []
241
+ # pids = self.api.get_recommend_playlists_ids()
242
+ # rec_playlists = [QQPlaylistModel.get(pid) for pid in pids]
243
+ playlists = self.api.recommend_playlists()
244
+ for pl in playlists:
245
+ pl["dissid"] = pl["content_id"]
246
+ pl["dissname"] = pl["title"]
247
+ pl["logo"] = pl["cover"]
248
+ return [_deserialize(playlist, QQPlaylistSchema) for playlist in playlists]
249
+
250
+ def current_user_list_playlists(self):
251
+ user = self.get_current_user()
252
+ if user is None:
253
+ return []
254
+ playlists = self._model_cache_get_or_fetch(user, "playlists")
255
+ return playlists
256
+
257
+ def current_user_fav_create_songs_rd(self):
258
+ user = self.get_current_user()
259
+ if user is None:
260
+ return create_reader([])
261
+ fav_pid = self._model_cache_get_or_fetch(user, "fav_pid")
262
+ playlist = self.playlist_get(fav_pid)
263
+ reader = create_reader(self.playlist_create_songs_rd(playlist))
264
+ return reader.readall()
265
+
266
+ def current_user_fav_create_albums_rd(self):
267
+ user = self.get_current_user()
268
+ if user is None:
269
+ return create_reader([])
270
+ # TODO: fetch more if total count > 100
271
+ albums = self.api.user_favorite_albums(user.identifier)
272
+ return [_deserialize(album, _UserAlbumSchema) for album in albums]
273
+
274
+ def current_user_fav_create_artists_rd(self):
275
+ user = self.get_current_user()
276
+ if user is None:
277
+ return create_reader([])
278
+ # TODO: fetch more if total count > 100
279
+ mid = self._model_cache_get_or_fetch(user, "mid")
280
+ artists = self.api.user_favorite_artists(user.identifier, mid)
281
+ return [_deserialize(artist, _UserArtistSchema) for artist in artists]
282
+
283
+ def current_user_fav_create_playlists_rd(self):
284
+ user = self.get_current_user()
285
+ if user is None:
286
+ return create_reader([])
287
+ mid = self._model_cache_get_or_fetch(user, "mid")
288
+ playlists = self.api.user_favorite_playlists(user.identifier, mid)
289
+ return [_deserialize(playlist, QQPlaylistSchema) for playlist in playlists]
290
+
291
+ def has_current_user(self):
292
+ return self._user is not None
293
+
294
+ def get_current_user(self):
295
+ return self._user
296
+
297
+ def song_list_similar(self, song):
298
+ data_songs = self.api.song_similar(int(song.identifier))
299
+ return [_deserialize(data_song, QQSongSchema) for data_song in data_songs]
300
+
301
+
302
+ def _deserialize(data, schema_cls):
303
+ schema = schema_cls()
304
+ obj = schema.load(data)
305
+ return obj
306
+
307
+
308
+ def create_g(func, identifier, schema):
309
+ data = func(identifier, page=1)
310
+ total = int(data["totalNum"] if schema == _ArtistSongSchema else data["total"])
311
+
312
+ def g():
313
+ nonlocal data
314
+ if data is None:
315
+ yield from ()
316
+ else:
317
+ page = 1
318
+ while data["songList"] if schema == _ArtistSongSchema else data["list"]:
319
+ obj_data_list = (
320
+ data["songList"] if schema == _ArtistSongSchema else data["list"]
321
+ )
322
+ for obj_data in obj_data_list:
323
+ obj = _deserialize(obj_data, schema)
324
+ yield obj
325
+ page += 1
326
+ data = func(identifier, page)
327
+
328
+ return SequentialReader(g(), total)
329
+
330
+
331
+ def search(keyword, **kwargs):
332
+ type_ = SearchType.parse(kwargs["type_"])
333
+ if type_ == SearchType.pl:
334
+ data = provider.api.search_playlists(keyword)
335
+ playlists = [_deserialize(playlist, _BriefPlaylistSchema) for playlist in data]
336
+ return SimpleSearchResult(q=keyword, playlists=playlists)
337
+ else:
338
+ type_type_map = {
339
+ SearchType.so: 0,
340
+ SearchType.al: 8,
341
+ SearchType.ar: 9,
342
+ }
343
+ data = provider.api.search(keyword, type_=type_type_map[type_])
344
+ if type_ == SearchType.so:
345
+ songs = [_deserialize(song, QQSongSchema) for song in data]
346
+ return SimpleSearchResult(q=keyword, songs=songs)
347
+ elif type_ == SearchType.al:
348
+ albums = [_deserialize(album, _BriefAlbumSchema) for album in data]
349
+ return SimpleSearchResult(q=keyword, albums=albums)
350
+ else:
351
+ artists = [_deserialize(artist, _BriefArtistSchema) for artist in data]
352
+ return SimpleSearchResult(q=keyword, artists=artists)
353
+
354
+
355
+ provider = QQProvider()
356
+ provider.search = search
357
+
358
+
359
+ from .schemas import ( # noqa
360
+ QQSongSchema,
361
+ QQArtistSchema,
362
+ _ArtistSongSchema,
363
+ _BriefAlbumSchema,
364
+ _UserArtistSchema,
365
+ _BriefArtistSchema,
366
+ _BriefPlaylistSchema,
367
+ QQAlbumSchema,
368
+ QQPlaylistSchema,
369
+ QQUserSchema,
370
+ _UserAlbumSchema,
371
+ ) # noqa