fuo-qqmusic 0.5.1__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.1 → fuo_qqmusic-1.0}/PKG-INFO +1 -1
  2. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/README.md +4 -0
  3. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/__init__.py +4 -4
  4. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/api.py +10 -23
  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.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/PKG-INFO +1 -1
  9. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/SOURCES.txt +1 -5
  10. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/setup.py +1 -1
  11. fuo_qqmusic-0.5.1/fuo_qqmusic/models.py +0 -363
  12. fuo_qqmusic-0.5.1/fuo_qqmusic/page_daily_recommendation.py +0 -23
  13. fuo_qqmusic-0.5.1/fuo_qqmusic/page_explore.py +0 -81
  14. fuo_qqmusic-0.5.1/fuo_qqmusic/page_fav.py +0 -55
  15. fuo_qqmusic-0.5.1/fuo_qqmusic/provider.py +0 -47
  16. fuo_qqmusic-0.5.1/fuo_qqmusic/schemas.py +0 -242
  17. fuo_qqmusic-0.5.1/fuo_qqmusic/ui.py +0 -133
  18. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/assets/icon.svg +0 -0
  19. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/consts.py +0 -0
  20. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/excs.py +0 -0
  21. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/dependency_links.txt +0 -0
  22. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/entry_points.txt +0 -0
  23. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/requires.txt +0 -0
  24. {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/top_level.txt +0 -0
  25. {fuo_qqmusic-0.5.1 → 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.1
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,6 +21,10 @@ pip3 install fuo-qqmusic
21
21
 
22
22
  ## changelog
23
23
 
24
+ ### 1.0 (2024-01-1)
25
+ - 使用 FeelUOwn 新接口
26
+ - 支持发现,推荐等接口
27
+
24
28
  ### 0.5.1 (2023-08-14)
25
29
  - 恢复部分接口(0.5.0 移除了一些实际上可用的接口)
26
30
 
@@ -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
@@ -311,13 +310,19 @@ class API(object):
311
310
  'HostUin': mid
312
311
  }},
313
312
  'comm': {
314
- '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()),
315
317
  'uin': uid,
316
318
  'format': 'json',
319
+ 'notice': 0,
320
+ 'platform': 'yqq.json',
321
+ 'needNewCode': 1,
317
322
  }
318
323
  }
319
324
  js = self.rpc(payload)
320
- return js['req_0']['data']['List']
325
+ return js['req_1']['data']['List']
321
326
 
322
327
  def user_favorite_albums(self, uid, start=0, end=100):
323
328
  url = api_base_url + '/fav/fcgi-bin/fcg_get_profile_order_asset.fcg'
@@ -351,30 +356,12 @@ class API(object):
351
356
  }
352
357
 
353
358
  resp = requests.get(url, params=params, headers=self._headers,
354
- timeout=self._timeout)
359
+ cookies=self._cookies, timeout=self._timeout)
355
360
  js = resp.json()
356
361
  if js['code'] != 0:
357
362
  raise CodeShouldBe0(js)
358
363
  return js['data']['cdlist']
359
364
 
360
- def get_recommend_songs_pid(self):
361
- data = {
362
- 'req_0': {
363
- 'module': 'recommend.RecommendFeedServer',
364
- 'method': 'get_recommend_feed',
365
- 'param': {
366
- 'direction': 0,
367
- 'page': 1,
368
- 'v_cache': [],
369
- 'v_uniq': [],
370
- 's_num': 0
371
- }
372
- },
373
- }
374
- js = self.rpc(data)
375
- disstid = js['req_0']['data']['v_shelf'][0]['v_niche'][0]['v_card'][1]['id']
376
- return disstid
377
-
378
365
  def recommend_playlists(self):
379
366
  data = {
380
367
  'recomPlaylist': {
@@ -434,7 +421,7 @@ class API(object):
434
421
  }
435
422
  url = 'https://u.y.qq.com/cgi-bin/musicu.fcg'
436
423
  resp = requests.get(url, params=params, headers=self._headers,
437
- timeout=self._timeout)
424
+ cookies=self._cookies, timeout=self._timeout)
438
425
  js = resp.json()
439
426
  CodeShouldBe0.check(js)
440
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
@@ -0,0 +1,102 @@
1
+ import json
2
+ import logging
3
+ import os
4
+
5
+ from feeluown.utils.dispatch import Signal
6
+ from feeluown.utils.aio import run_fn
7
+ from feeluown.consts import DATA_DIR
8
+ from feeluown.gui.widgets.login import CookiesLoginDialog, InvalidCookies
9
+ from feeluown.gui.provider_ui import AbstractProviderUi
10
+ from feeluown.app.gui_app import GuiApp
11
+
12
+ from .provider import provider
13
+ from .excs import QQIOError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
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
+ class ProviderUI(AbstractProviderUi):
29
+ def __init__(self, app: GuiApp):
30
+ self._app = app
31
+ self._login_event = Signal()
32
+
33
+ @property
34
+ def provider(self):
35
+ return provider
36
+
37
+ def get_colorful_svg(self) -> str:
38
+ return os.path.join(os.path.dirname(__file__), 'assets', 'icon.svg')
39
+
40
+ def login_or_go_home(self):
41
+ if provider._user is None:
42
+ # According to #14, we have two ways to login:
43
+ # 1. the default way, as the code shows
44
+ # 2. a way for VIP user(maybe):
45
+ # - url: https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=1006102
46
+ # &daid=384&low_login=1&pt_no_auth=1
47
+ # &s_url=https://y.qq.com/vip/daren_recruit/apply.html&style=40
48
+ #
49
+ # - keys: ['skey']
50
+ url = os.getenv('FUO_QQMUSIC_LOGIN_URL', 'https://y.qq.com')
51
+ keys = os.getenv('FUO_QQMUSIC_LOGIN_COOKIE_KEYS', 'qqmusic_key').split(',')
52
+ self._dialog = LoginDialog(url, keys)
53
+ self._dialog.login_succeed.connect(self.on_login_succeed)
54
+ self._dialog.show()
55
+ self._dialog.autologin()
56
+ else:
57
+ logger.info('already logged in')
58
+ self.login_event.emit(self, 2)
59
+
60
+ @property
61
+ def login_event(self):
62
+ return self._login_event
63
+
64
+ def on_login_succeed(self):
65
+ del self._dialog
66
+ self.login_event.emit(self, 1)
67
+
68
+
69
+ class LoginDialog(CookiesLoginDialog):
70
+
71
+ def setup_user(self, user):
72
+ provider._user = user
73
+
74
+ async def user_from_cookies(self, cookies):
75
+ if not cookies: # is None or empty
76
+ raise InvalidCookies('empty cookies')
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:
90
+ return user
91
+
92
+ def load_user_cookies(self):
93
+ return read_cookies()
94
+
95
+ def dump_user_cookies(self, user, cookies):
96
+ js = {
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)