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.
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/PKG-INFO +1 -1
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/README.md +4 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/__init__.py +4 -4
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/api.py +10 -23
- fuo_qqmusic-1.0/fuo_qqmusic/provider.py +371 -0
- fuo_qqmusic-1.0/fuo_qqmusic/provider_ui.py +102 -0
- fuo_qqmusic-1.0/fuo_qqmusic/schemas.py +296 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/PKG-INFO +1 -1
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/SOURCES.txt +1 -5
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/setup.py +1 -1
- fuo_qqmusic-0.5.1/fuo_qqmusic/models.py +0 -363
- fuo_qqmusic-0.5.1/fuo_qqmusic/page_daily_recommendation.py +0 -23
- fuo_qqmusic-0.5.1/fuo_qqmusic/page_explore.py +0 -81
- fuo_qqmusic-0.5.1/fuo_qqmusic/page_fav.py +0 -55
- fuo_qqmusic-0.5.1/fuo_qqmusic/provider.py +0 -47
- fuo_qqmusic-0.5.1/fuo_qqmusic/schemas.py +0 -242
- fuo_qqmusic-0.5.1/fuo_qqmusic/ui.py +0 -133
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/assets/icon.svg +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/consts.py +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic/excs.py +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/dependency_links.txt +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/entry_points.txt +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/requires.txt +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/top_level.txt +0 -0
- {fuo_qqmusic-0.5.1 → fuo_qqmusic-1.0}/setup.cfg +0 -0
|
@@ -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 .
|
|
19
|
-
|
|
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
|
-
|
|
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['
|
|
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)
|