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.
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/PKG-INFO +1 -1
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/README.md +7 -7
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/__init__.py +4 -4
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/api.py +105 -27
- 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.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/PKG-INFO +1 -1
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/SOURCES.txt +1 -5
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/setup.py +1 -1
- fuo_qqmusic-0.5.0/fuo_qqmusic/models.py +0 -355
- fuo_qqmusic-0.5.0/fuo_qqmusic/page_daily_recommendation.py +0 -23
- fuo_qqmusic-0.5.0/fuo_qqmusic/page_explore.py +0 -81
- fuo_qqmusic-0.5.0/fuo_qqmusic/page_fav.py +0 -55
- fuo_qqmusic-0.5.0/fuo_qqmusic/provider.py +0 -47
- fuo_qqmusic-0.5.0/fuo_qqmusic/schemas.py +0 -242
- fuo_qqmusic-0.5.0/fuo_qqmusic/ui.py +0 -133
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/assets/icon.svg +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/consts.py +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic/excs.py +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/dependency_links.txt +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/entry_points.txt +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/requires.txt +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/fuo_qqmusic.egg-info/top_level.txt +0 -0
- {fuo_qqmusic-0.5.0 → fuo_qqmusic-1.0}/setup.cfg +0 -0
|
@@ -21,41 +21,41 @@ pip3 install fuo-qqmusic
|
|
|
21
21
|
|
|
22
22
|
## changelog
|
|
23
23
|
|
|
24
|
-
###
|
|
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 .
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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['
|
|
325
|
+
return js['req_1']['data']['List']
|
|
273
326
|
|
|
274
327
|
def user_favorite_albums(self, uid, start=0, end=100):
|
|
275
|
-
|
|
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
|
-
|
|
346
|
+
url = api_base_url + '/fav/fcgi-bin/fcg_get_profile_order_asset.fcg'
|
|
279
347
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
'
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|