musicdl 2.1.11__py3-none-any.whl → 2.7.3__py3-none-any.whl
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.
- musicdl/__init__.py +5 -5
- musicdl/modules/__init__.py +10 -3
- musicdl/modules/common/__init__.py +2 -0
- musicdl/modules/common/gdstudio.py +204 -0
- musicdl/modules/js/__init__.py +1 -0
- musicdl/modules/js/youtube/__init__.py +2 -0
- musicdl/modules/js/youtube/botguard.js +1 -0
- musicdl/modules/js/youtube/jsinterp.py +902 -0
- musicdl/modules/js/youtube/runner.js +2 -0
- musicdl/modules/sources/__init__.py +41 -10
- musicdl/modules/sources/apple.py +207 -0
- musicdl/modules/sources/base.py +256 -28
- musicdl/modules/sources/bilibili.py +118 -0
- musicdl/modules/sources/buguyy.py +148 -0
- musicdl/modules/sources/fangpi.py +153 -0
- musicdl/modules/sources/fivesing.py +108 -0
- musicdl/modules/sources/gequbao.py +148 -0
- musicdl/modules/sources/jamendo.py +108 -0
- musicdl/modules/sources/joox.py +104 -68
- musicdl/modules/sources/kugou.py +129 -76
- musicdl/modules/sources/kuwo.py +188 -68
- musicdl/modules/sources/lizhi.py +107 -0
- musicdl/modules/sources/migu.py +172 -66
- musicdl/modules/sources/mitu.py +140 -0
- musicdl/modules/sources/mp3juice.py +264 -0
- musicdl/modules/sources/netease.py +163 -115
- musicdl/modules/sources/qianqian.py +125 -77
- musicdl/modules/sources/qq.py +232 -94
- musicdl/modules/sources/tidal.py +342 -0
- musicdl/modules/sources/ximalaya.py +256 -0
- musicdl/modules/sources/yinyuedao.py +144 -0
- musicdl/modules/sources/youtube.py +238 -0
- musicdl/modules/utils/__init__.py +12 -4
- musicdl/modules/utils/appleutils.py +563 -0
- musicdl/modules/utils/data.py +107 -0
- musicdl/modules/utils/logger.py +211 -58
- musicdl/modules/utils/lyric.py +73 -0
- musicdl/modules/utils/misc.py +335 -23
- musicdl/modules/utils/modulebuilder.py +75 -0
- musicdl/modules/utils/neteaseutils.py +81 -0
- musicdl/modules/utils/qqutils.py +184 -0
- musicdl/modules/utils/quarkparser.py +105 -0
- musicdl/modules/utils/songinfoutils.py +54 -0
- musicdl/modules/utils/tidalutils.py +738 -0
- musicdl/modules/utils/youtubeutils.py +3606 -0
- musicdl/musicdl.py +184 -86
- musicdl-2.7.3.dist-info/LICENSE +203 -0
- musicdl-2.7.3.dist-info/METADATA +704 -0
- musicdl-2.7.3.dist-info/RECORD +53 -0
- {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/WHEEL +5 -5
- musicdl-2.7.3.dist-info/entry_points.txt +2 -0
- musicdl/modules/sources/baiduFlac.py +0 -69
- musicdl/modules/sources/xiami.py +0 -104
- musicdl/modules/utils/downloader.py +0 -80
- musicdl-2.1.11.dist-info/LICENSE +0 -22
- musicdl-2.1.11.dist-info/METADATA +0 -82
- musicdl-2.1.11.dist-info/RECORD +0 -24
- {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/top_level.txt +0 -0
- {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/zip-safe +0 -0
|
@@ -1,124 +1,172 @@
|
|
|
1
1
|
'''
|
|
2
2
|
Function:
|
|
3
|
-
|
|
3
|
+
Implementation of NeteaseMusicClient: https://music.163.com/
|
|
4
4
|
Author:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
7
|
Charles的皮卡丘
|
|
8
8
|
'''
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
from .
|
|
14
|
-
from ..utils.
|
|
15
|
-
from
|
|
9
|
+
import json
|
|
10
|
+
import copy
|
|
11
|
+
import random
|
|
12
|
+
from .base import BaseMusicClient
|
|
13
|
+
from rich.progress import Progress
|
|
14
|
+
from ..utils.neteaseutils import EapiCryptoUtils
|
|
15
|
+
from ..utils import resp2json, seconds2hms, legalizestring, safeextractfromdict, usesearchheaderscookies, SongInfo
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
'''
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self.modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
|
|
26
|
-
self.nonce = '0CoJUm6Qyw8W8jud'
|
|
27
|
-
self.pubKey = '010001'
|
|
28
|
-
def get(self, text):
|
|
29
|
-
text = json.dumps(text)
|
|
30
|
-
secKey = self.__createSecretKey(16)
|
|
31
|
-
encText = self.__aesEncrypt(self.__aesEncrypt(text, self.nonce), secKey)
|
|
32
|
-
encSecKey = self.__rsaEncrypt(secKey, self.pubKey, self.modulus)
|
|
33
|
-
post_data = {
|
|
34
|
-
'params': encText,
|
|
35
|
-
'encSecKey': encSecKey
|
|
36
|
-
}
|
|
37
|
-
return post_data
|
|
38
|
-
def __aesEncrypt(self, text, secKey):
|
|
39
|
-
pad = 16 - len(text) % 16
|
|
40
|
-
if isinstance(text, bytes):
|
|
41
|
-
text = text.decode('utf-8')
|
|
42
|
-
text = text + str(pad * chr(pad))
|
|
43
|
-
secKey = secKey.encode('utf-8')
|
|
44
|
-
encryptor = AES.new(secKey, 2, b'0102030405060708')
|
|
45
|
-
text = text.encode('utf-8')
|
|
46
|
-
ciphertext = encryptor.encrypt(text)
|
|
47
|
-
ciphertext = base64.b64encode(ciphertext)
|
|
48
|
-
return ciphertext
|
|
49
|
-
def __rsaEncrypt(self, text, pubKey, modulus):
|
|
50
|
-
text = text[::-1]
|
|
51
|
-
rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16)
|
|
52
|
-
return format(rs, 'x').zfill(256)
|
|
53
|
-
def __createSecretKey(self, size):
|
|
54
|
-
return (''.join(map(lambda xx: (hex(ord(xx))[2:]), str(os.urandom(size)))))[0: 16]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'''网易云音乐下载类'''
|
|
58
|
-
class netease(Base):
|
|
59
|
-
def __init__(self, config, logger_handle, **kwargs):
|
|
60
|
-
super(netease, self).__init__(config, logger_handle, **kwargs)
|
|
61
|
-
self.source = 'netease'
|
|
62
|
-
self.cracker = Cracker()
|
|
63
|
-
self.__initialize()
|
|
64
|
-
'''歌曲搜索'''
|
|
65
|
-
def search(self, keyword):
|
|
66
|
-
self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword))
|
|
67
|
-
cfg = self.config.copy()
|
|
68
|
-
params = {
|
|
69
|
-
's': keyword,
|
|
70
|
-
'type': '1',
|
|
71
|
-
'offset': '0',
|
|
72
|
-
'sub': 'false',
|
|
73
|
-
'limit': cfg['search_size_per_source']
|
|
74
|
-
}
|
|
75
|
-
response = self.session.post(self.search_url, headers=self.headers, params=params, data=self.cracker.get(params))
|
|
76
|
-
all_items = response.json()['result']['songs']
|
|
77
|
-
songinfos = []
|
|
78
|
-
for item in all_items:
|
|
79
|
-
if item['privilege']['fl'] == 0: continue
|
|
80
|
-
for q in ['h', 'm', 'l']:
|
|
81
|
-
params = {
|
|
82
|
-
'ids': [item['id']],
|
|
83
|
-
'br': item[q]['br'],
|
|
84
|
-
'csrf_token': ''
|
|
85
|
-
}
|
|
86
|
-
response = self.session.post(self.player_url, headers=self.headers, data=self.cracker.get(params))
|
|
87
|
-
response_json = response.json()
|
|
88
|
-
if response_json.get('code') == 200: break
|
|
89
|
-
if response_json.get('code') != 200: continue
|
|
90
|
-
download_url = response_json['data'][0]['url']
|
|
91
|
-
if not download_url: continue
|
|
92
|
-
filesize = str(round(int(item[q]['size'])/1024/1024, 2)) + 'MB'
|
|
93
|
-
ext = download_url.split('.')[-1]
|
|
94
|
-
duration = int(item.get('dt', 0) / 1000)
|
|
95
|
-
songinfo = {
|
|
96
|
-
'source': self.source,
|
|
97
|
-
'songid': str(item['id']),
|
|
98
|
-
'singers': filterBadCharacter(','.join([s.get('name', '') for s in item.get('ar')])),
|
|
99
|
-
'album': filterBadCharacter(item.get('al', {}).get('name', '-')),
|
|
100
|
-
'songname': filterBadCharacter(item.get('name', '-')),
|
|
101
|
-
'savedir': cfg['savedir'],
|
|
102
|
-
'savename': '_'.join([self.source, filterBadCharacter(item.get('name', '-'))]),
|
|
103
|
-
'download_url': download_url,
|
|
104
|
-
'filesize': filesize,
|
|
105
|
-
'ext': ext,
|
|
106
|
-
'duration': seconds2hms(duration)
|
|
107
|
-
}
|
|
108
|
-
songinfos.append(songinfo)
|
|
109
|
-
return songinfos
|
|
110
|
-
'''初始化'''
|
|
111
|
-
def __initialize(self):
|
|
112
|
-
self.headers = {
|
|
113
|
-
'Accept': '*/*',
|
|
114
|
-
'Accept-Encoding': 'gzip,deflate,sdch',
|
|
115
|
-
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
|
|
116
|
-
'Connection': 'keep-alive',
|
|
117
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
118
|
-
'Host': 'music.163.com',
|
|
119
|
-
'Origin': 'https://music.163.com',
|
|
18
|
+
'''NeteaseMusicClient'''
|
|
19
|
+
class NeteaseMusicClient(BaseMusicClient):
|
|
20
|
+
source = 'NeteaseMusicClient'
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
super(NeteaseMusicClient, self).__init__(**kwargs)
|
|
23
|
+
self.default_search_headers = {
|
|
24
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
120
25
|
'Referer': 'https://music.163.com/',
|
|
121
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36'
|
|
122
26
|
}
|
|
123
|
-
self.
|
|
124
|
-
self.
|
|
27
|
+
self.default_download_headers = {}
|
|
28
|
+
self.default_headers = self.default_search_headers
|
|
29
|
+
default_cookies = {'MUSIC_U': '1eb9ce22024bb666e99b6743b2222f29ef64a9e88fda0fd5754714b900a5d70d993166e004087dd3b95085f6a85b059f5e9aba41e3f2646e3cebdbec0317df58c119e5'}
|
|
30
|
+
if not self.default_search_cookies: self.default_search_cookies = default_cookies
|
|
31
|
+
if not self.default_download_cookies: self.default_download_cookies = default_cookies
|
|
32
|
+
self._initsession()
|
|
33
|
+
'''_parsewithcggapi'''
|
|
34
|
+
def _parsewithcggapi(self, search_result: dict, request_overrides: dict = None):
|
|
35
|
+
# init
|
|
36
|
+
request_overrides, song_id = request_overrides or {}, search_result['id']
|
|
37
|
+
# _safefetchfilesize
|
|
38
|
+
def _safefetchfilesize(meta: dict):
|
|
39
|
+
if not isinstance(meta, dict): return 0
|
|
40
|
+
file_size = str(meta.get('size', '0.00MB'))
|
|
41
|
+
file_size = file_size.removesuffix('MB').strip()
|
|
42
|
+
try: return float(file_size)
|
|
43
|
+
except: return 0
|
|
44
|
+
# parse
|
|
45
|
+
for quality in ['jymaster', 'sky', 'jyeffect', 'hires', 'lossless', 'exhigh', 'standard']:
|
|
46
|
+
try:
|
|
47
|
+
try:
|
|
48
|
+
resp = self.get(url=f'https://api-v1.cenguigui.cn/api/netease/music_v1.php?id={song_id}&type=json&level={quality}', timeout=10, **request_overrides)
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
except:
|
|
51
|
+
resp = self.get(url=f'https://api.cenguigui.cn/api/netease/music_v1.php?id={song_id}&type=json&level={quality}', timeout=10, **request_overrides)
|
|
52
|
+
resp.raise_for_status()
|
|
53
|
+
download_result = resp2json(resp=resp)
|
|
54
|
+
if 'data' not in download_result or (_safefetchfilesize(download_result['data']) < 0.01): continue
|
|
55
|
+
except:
|
|
56
|
+
continue
|
|
57
|
+
download_url: str = download_result['data'].get('url', '')
|
|
58
|
+
if not download_url: continue
|
|
59
|
+
song_info = SongInfo(
|
|
60
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
61
|
+
ext=download_url.split('.')[-1].split('?')[0], raw_data={'search': search_result, 'download': download_result},
|
|
62
|
+
)
|
|
63
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
64
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
65
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
66
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
67
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
68
|
+
if song_info.with_valid_download_url: break
|
|
69
|
+
# return
|
|
70
|
+
return song_info, quality
|
|
71
|
+
'''_constructsearchurls'''
|
|
72
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
73
|
+
# init
|
|
74
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
75
|
+
# search rules
|
|
76
|
+
default_rule = {'s': keyword, 'type': 1, 'limit': 10, 'offset': 0}
|
|
77
|
+
default_rule.update(rule)
|
|
78
|
+
# construct search urls based on search rules
|
|
79
|
+
base_url = 'https://music.163.com/api/cloudsearch/pc'
|
|
80
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
81
|
+
while self.search_size_per_source > count:
|
|
82
|
+
page_rule = copy.deepcopy(default_rule)
|
|
83
|
+
page_rule['limit'] = page_size
|
|
84
|
+
page_rule['offset'] = int(count // page_size) * page_size
|
|
85
|
+
search_urls.append({'url': base_url, 'data': page_rule})
|
|
86
|
+
count += page_size
|
|
87
|
+
# return
|
|
88
|
+
return search_urls
|
|
89
|
+
'''_search'''
|
|
90
|
+
@usesearchheaderscookies
|
|
91
|
+
def _search(self, keyword: str = '', search_url: dict = {}, request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
92
|
+
# init
|
|
93
|
+
request_overrides = request_overrides or {}
|
|
94
|
+
search_meta = copy.deepcopy(search_url)
|
|
95
|
+
search_url = search_meta.pop('url')
|
|
96
|
+
# successful
|
|
97
|
+
try:
|
|
98
|
+
# --search results
|
|
99
|
+
resp = self.post(search_url, **search_meta, **request_overrides)
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
search_results = resp2json(resp)['result']['songs']
|
|
102
|
+
for search_result in search_results:
|
|
103
|
+
# --download results
|
|
104
|
+
if not isinstance(search_result, dict) or ('id' not in search_result):
|
|
105
|
+
continue
|
|
106
|
+
song_info = SongInfo(source=self.source)
|
|
107
|
+
# ----try _parsewithcggapi first
|
|
108
|
+
try:
|
|
109
|
+
song_info_cgg, quality_cgg = self._parsewithcggapi(search_result, request_overrides)
|
|
110
|
+
except:
|
|
111
|
+
song_info_cgg, quality_cgg = SongInfo(source=self.source), "standard"
|
|
112
|
+
# ----general parse with official API
|
|
113
|
+
qualties = ["jymaster", "jyeffect", "sky", "hires", "lossless", "exhigh", "standard"]
|
|
114
|
+
for quality_idx, quality in enumerate(qualties):
|
|
115
|
+
if quality_idx >= qualties.index(quality_cgg) and song_info_cgg.with_valid_download_url: song_info = song_info_cgg; break
|
|
116
|
+
header = {"os": "pc", "appver": "", "osver": "", "deviceId": "pyncm!"}
|
|
117
|
+
header["requestId"] = str(random.randrange(20000000, 30000000))
|
|
118
|
+
params = {'ids': [search_result['id']], 'level': quality, 'encodeType': 'flac', 'header': json.dumps(header)}
|
|
119
|
+
if quality == 'sky': params['immerseType'] = 'c51'
|
|
120
|
+
params = EapiCryptoUtils.encryptparams(url='https://interface3.music.163.com/eapi/song/enhance/player/url/v1', payload=params)
|
|
121
|
+
try:
|
|
122
|
+
resp = self.post('https://interface3.music.163.com/eapi/song/enhance/player/url/v1', data={"params": params}, **request_overrides)
|
|
123
|
+
resp.raise_for_status()
|
|
124
|
+
download_result: dict = resp2json(resp)
|
|
125
|
+
except:
|
|
126
|
+
continue
|
|
127
|
+
if (download_result.get('code') not in [200, '200']) or ('data' not in download_result) or (not download_result['data']) or \
|
|
128
|
+
(not isinstance(download_result['data'], list)) or (not isinstance(download_result['data'][0], dict)):
|
|
129
|
+
continue
|
|
130
|
+
download_url: str = download_result['data'][0].get('url', '')
|
|
131
|
+
if not download_url: continue
|
|
132
|
+
song_info = SongInfo(
|
|
133
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
134
|
+
ext=download_url.split('.')[-1].split('?')[0], raw_data={'search': search_result, 'download': download_result},
|
|
135
|
+
)
|
|
136
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
137
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
138
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
139
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
140
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
141
|
+
if song_info.with_valid_download_url: break
|
|
142
|
+
if not song_info.with_valid_download_url: continue
|
|
143
|
+
# ----parse more information
|
|
144
|
+
song_info.update(dict(
|
|
145
|
+
duration=seconds2hms(search_result.get('dt', 0) / 1000 if isinstance(search_result.get('dt', 0), (int, float)) else 0),
|
|
146
|
+
song_name=legalizestring(search_result.get('name', 'NULL'), replace_null_string='NULL'),
|
|
147
|
+
singers=legalizestring(', '.join([singer.get('name', 'NULL') for singer in search_result.get('ar', [])]), replace_null_string='NULL'),
|
|
148
|
+
album=legalizestring(safeextractfromdict(search_result, ['al', 'name'], 'NULL'), replace_null_string='NULL'),
|
|
149
|
+
identifier=search_result['id'],
|
|
150
|
+
))
|
|
151
|
+
# --lyric results
|
|
152
|
+
data = {'id': search_result['id'], 'cp': 'false', 'tv': '0', 'lv': '0', 'rv': '0', 'kv': '0', 'yv': '0', 'ytv': '0', 'yrv': '0'}
|
|
153
|
+
try:
|
|
154
|
+
resp = self.post('https://interface3.music.163.com/api/song/lyric', data=data, **request_overrides)
|
|
155
|
+
resp.raise_for_status()
|
|
156
|
+
lyric_result: dict = resp2json(resp)
|
|
157
|
+
lyric = lyric_result.get('lrc', {}).get('lyric', 'NULL') or lyric_result.get('tlyric', {}).get('lyric', 'NULL')
|
|
158
|
+
except:
|
|
159
|
+
lyric_result, lyric = dict(), 'NULL'
|
|
160
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
161
|
+
song_info.lyric = lyric
|
|
162
|
+
# --append to song_infos
|
|
163
|
+
song_infos.append(song_info)
|
|
164
|
+
# --judgement for search_size
|
|
165
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
166
|
+
# --update progress
|
|
167
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
168
|
+
# failure
|
|
169
|
+
except Exception as err:
|
|
170
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
171
|
+
# return
|
|
172
|
+
return song_infos
|
|
@@ -1,88 +1,136 @@
|
|
|
1
1
|
'''
|
|
2
2
|
Function:
|
|
3
|
-
|
|
3
|
+
Implementation of QianqianMusicClient: http://music.taihe.com/
|
|
4
4
|
Author:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
7
|
Charles的皮卡丘
|
|
8
8
|
'''
|
|
9
|
+
import re
|
|
9
10
|
import time
|
|
11
|
+
import copy
|
|
10
12
|
import hashlib
|
|
11
|
-
import
|
|
12
|
-
from .
|
|
13
|
-
from
|
|
13
|
+
from .base import BaseMusicClient
|
|
14
|
+
from urllib.parse import urlencode
|
|
15
|
+
from rich.progress import Progress
|
|
16
|
+
from ..utils import byte2mb, resp2json, seconds2hms, legalizestring, safeextractfromdict, usesearchheaderscookies, SongInfo
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
'''
|
|
17
|
-
class
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
self.
|
|
21
|
-
self.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'timestamp': str(int(time.time()))
|
|
19
|
+
'''QianqianMusicClient'''
|
|
20
|
+
class QianqianMusicClient(BaseMusicClient):
|
|
21
|
+
source = 'QianqianMusicClient'
|
|
22
|
+
def __init__(self, **kwargs):
|
|
23
|
+
super(QianqianMusicClient, self).__init__(**kwargs)
|
|
24
|
+
self.appid = '16073360'
|
|
25
|
+
self.default_search_headers = {
|
|
26
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
27
|
+
'Referer': 'https://music.91q.com/',
|
|
28
|
+
'From': 'Web',
|
|
29
|
+
'Accept': 'application/json, text/plain, */*',
|
|
30
|
+
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
31
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
songinfos = []
|
|
34
|
-
for item in all_items:
|
|
35
|
-
params = {
|
|
36
|
-
'sign': self.__calcSign(keyword),
|
|
37
|
-
'TSID': item['TSID'],
|
|
38
|
-
'timestamp': str(int(time.time())),
|
|
39
|
-
'from': 'web',
|
|
40
|
-
's_protocol': '1',
|
|
41
|
-
}
|
|
42
|
-
response = self.session.get(self.tracklink_url, headers=self.headers, params=params)
|
|
43
|
-
response_json = response.json()
|
|
44
|
-
if response_json.get('errno') != 22000: continue
|
|
45
|
-
download_url = response_json['data']['path']
|
|
46
|
-
if not download_url: continue
|
|
47
|
-
filesize = str(round(int(response_json['data']['size'])/1024/1024, 2)) + 'MB'
|
|
48
|
-
ext = response_json['data']['format']
|
|
49
|
-
duration = int(response_json['data']['duration'])
|
|
50
|
-
songinfo = {
|
|
51
|
-
'source': self.source,
|
|
52
|
-
'songid': str(item['id']),
|
|
53
|
-
'singers': filterBadCharacter(item['artist'][0].get('name', '-')),
|
|
54
|
-
'album': filterBadCharacter(item.get('albumTitle', '-')),
|
|
55
|
-
'songname': filterBadCharacter(item.get('title', '-')).split('–')[0].strip(),
|
|
56
|
-
'savedir': cfg['savedir'],
|
|
57
|
-
'savename': '_'.join([self.source, filterBadCharacter(item.get('title', '-')).split('–')[0].strip()]),
|
|
58
|
-
'download_url': download_url,
|
|
59
|
-
'filesize': filesize,
|
|
60
|
-
'ext': ext,
|
|
61
|
-
'duration': seconds2hms(duration)
|
|
62
|
-
}
|
|
63
|
-
songinfos.append(songinfo)
|
|
64
|
-
if len(songinfos) == cfg['search_size_per_source']: break
|
|
65
|
-
return songinfos
|
|
66
|
-
'''计算sign值'''
|
|
67
|
-
def __calcSign(self, keyword):
|
|
68
|
-
secret = '0b50b02fd0d73a9c4c8c3a781c30845f'
|
|
69
|
-
e = {
|
|
70
|
-
'word': keyword,
|
|
71
|
-
'timestamp': str(int(time.time()))
|
|
72
|
-
}
|
|
73
|
-
n = list(e.keys())
|
|
74
|
-
n.sort()
|
|
75
|
-
i = f'{n[0]}={e[n[0]]}'
|
|
76
|
-
for r in range(1, len(n)):
|
|
77
|
-
o = n[r]
|
|
78
|
-
i += f'&{o}={e[o]}'
|
|
79
|
-
sign = hashlib.md5((i + secret).encode('utf-8')).hexdigest()
|
|
80
|
-
return sign
|
|
81
|
-
'''初始化'''
|
|
82
|
-
def __initialize(self):
|
|
83
|
-
self.headers = {
|
|
84
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
|
|
85
|
-
'Referer': 'https://music.taihe.com/'
|
|
33
|
+
self.default_download_headers = {
|
|
34
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
86
35
|
}
|
|
87
|
-
self.
|
|
88
|
-
self.
|
|
36
|
+
self.default_headers = self.default_search_headers
|
|
37
|
+
self._initsession()
|
|
38
|
+
'''_addsignandtstoparams'''
|
|
39
|
+
def _addsignandtstoparams(self, params: dict):
|
|
40
|
+
secret = '0b50b02fd0d73a9c4c8c3a781c30845f'
|
|
41
|
+
params['timestamp'] = str(int(time.time()))
|
|
42
|
+
keys = sorted(params.keys())
|
|
43
|
+
string = "&".join(f"{k}={params[k]}" for k in keys)
|
|
44
|
+
params['sign'] = hashlib.md5((string + secret).encode('utf-8')).hexdigest()
|
|
45
|
+
return params
|
|
46
|
+
'''_constructsearchurls'''
|
|
47
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
48
|
+
# init
|
|
49
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
50
|
+
# search rules
|
|
51
|
+
default_rule = {'word': keyword, 'type': '1', 'pageNo': '1', 'pageSize': '10', 'appid': self.appid}
|
|
52
|
+
default_rule.update(rule)
|
|
53
|
+
# construct search urls based on search rules
|
|
54
|
+
base_url = 'https://music.91q.com/v1/search?'
|
|
55
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
56
|
+
while self.search_size_per_source > count:
|
|
57
|
+
page_rule = copy.deepcopy(default_rule)
|
|
58
|
+
page_rule['pageSize'] = page_size
|
|
59
|
+
page_rule['pageNo'] = str(int(count // page_size) + 1)
|
|
60
|
+
page_rule = self._addsignandtstoparams(params=page_rule)
|
|
61
|
+
search_urls.append(base_url + urlencode(page_rule))
|
|
62
|
+
count += page_size
|
|
63
|
+
# return
|
|
64
|
+
return search_urls
|
|
65
|
+
'''_search'''
|
|
66
|
+
@usesearchheaderscookies
|
|
67
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
68
|
+
# init
|
|
69
|
+
request_overrides = request_overrides or {}
|
|
70
|
+
# successful
|
|
71
|
+
try:
|
|
72
|
+
# --search results
|
|
73
|
+
resp = self.get(search_url, **request_overrides)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
search_results = resp2json(resp)['data']['typeTrack']
|
|
76
|
+
for search_result in search_results:
|
|
77
|
+
# --download results
|
|
78
|
+
if not isinstance(search_result, dict) or ('TSID' not in search_result):
|
|
79
|
+
continue
|
|
80
|
+
song_info = SongInfo(source=self.source)
|
|
81
|
+
for rate in ['64', '128', '320', '3000'][::-1]:
|
|
82
|
+
params = {'TSID': search_result['TSID'], 'appid': self.appid, 'rate': rate}
|
|
83
|
+
params = self._addsignandtstoparams(params=params)
|
|
84
|
+
try:
|
|
85
|
+
resp = self.get("https://music.91q.com/v1/song/tracklink", params=params, **request_overrides)
|
|
86
|
+
resp.raise_for_status()
|
|
87
|
+
download_result: dict = resp2json(resp)
|
|
88
|
+
download_url = safeextractfromdict(download_result, ['data', 'path'], '')
|
|
89
|
+
if not download_url: continue
|
|
90
|
+
song_info = SongInfo(
|
|
91
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
92
|
+
raw_data={'search': search_result, 'download': download_result}, file_size_bytes=download_result['data'].get('size', 0),
|
|
93
|
+
file_size=byte2mb(download_result['data'].get('size', 0)), duration_s=download_result['data'].get('duration', 0),
|
|
94
|
+
duration = seconds2hms(download_result['data'].get('duration', 0)), ext=download_result['data'].get('format', 'mp3')
|
|
95
|
+
)
|
|
96
|
+
if song_info.with_valid_download_url: break
|
|
97
|
+
except:
|
|
98
|
+
continue
|
|
99
|
+
if not song_info.with_valid_download_url: continue
|
|
100
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
101
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
102
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
103
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
104
|
+
song_info.update(dict(
|
|
105
|
+
song_name=legalizestring(search_result.get('title', 'NULL'), replace_null_string='NULL'),
|
|
106
|
+
singers=legalizestring(', '.join([singer.get('name', 'NULL') for singer in search_result.get('artist', [])]), replace_null_string='NULL'),
|
|
107
|
+
album=legalizestring(search_result.get('albumTitle', 'NULL'), replace_null_string='NULL'),
|
|
108
|
+
identifier=search_result['TSID'],
|
|
109
|
+
))
|
|
110
|
+
# --lyric results
|
|
111
|
+
try:
|
|
112
|
+
resp = self.get(search_result['lyric'], **request_overrides)
|
|
113
|
+
resp.raise_for_status()
|
|
114
|
+
resp.encoding = 'utf-8'
|
|
115
|
+
lyric = resp.text or 'NULL'
|
|
116
|
+
lyric_result = dict(lyric=lyric)
|
|
117
|
+
if song_info.singers == 'NULL':
|
|
118
|
+
try:
|
|
119
|
+
song_info.singers = re.findall(r'\[ar:(.*?)\]', lyric)[0]
|
|
120
|
+
except:
|
|
121
|
+
song_info.singers = 'NULL'
|
|
122
|
+
except:
|
|
123
|
+
lyric_result, lyric = dict(), 'NULL'
|
|
124
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
125
|
+
song_info.lyric = lyric
|
|
126
|
+
# --append to song_infos
|
|
127
|
+
song_infos.append(song_info)
|
|
128
|
+
# --judgement for search_size
|
|
129
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
130
|
+
# --update progress
|
|
131
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
132
|
+
# failure
|
|
133
|
+
except Exception as err:
|
|
134
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
135
|
+
# return
|
|
136
|
+
return song_infos
|