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
musicdl/modules/utils/misc.py
CHANGED
|
@@ -1,39 +1,351 @@
|
|
|
1
1
|
'''
|
|
2
2
|
Function:
|
|
3
|
-
|
|
3
|
+
Implementation of common utils
|
|
4
4
|
Author:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
7
|
Charles的皮卡丘
|
|
8
8
|
'''
|
|
9
|
+
import re
|
|
9
10
|
import os
|
|
10
|
-
import
|
|
11
|
+
import html
|
|
12
|
+
import copy
|
|
13
|
+
import emoji
|
|
14
|
+
import errno
|
|
15
|
+
import pickle
|
|
16
|
+
import shutil
|
|
17
|
+
import bleach
|
|
18
|
+
import requests
|
|
19
|
+
import functools
|
|
20
|
+
import json_repair
|
|
21
|
+
import unicodedata
|
|
22
|
+
from io import BytesIO
|
|
23
|
+
from bs4 import BeautifulSoup
|
|
24
|
+
from mutagen import File as MutagenFile
|
|
25
|
+
from pathvalidate import sanitize_filepath, sanitize_filename
|
|
11
26
|
|
|
12
27
|
|
|
13
|
-
'''
|
|
14
|
-
def
|
|
15
|
-
if not
|
|
16
|
-
|
|
28
|
+
'''estimatedurationwithfilesizebr'''
|
|
29
|
+
def estimatedurationwithfilesizebr(file_size_bytes: int, br_kbps: float, return_seconds: bool = False) -> str:
|
|
30
|
+
if not file_size_bytes or not br_kbps or br_kbps <= 0: return "-:-:-"
|
|
31
|
+
total_bits = file_size_bytes * 8
|
|
32
|
+
duration_seconds = int(total_bits / (br_kbps * 1000))
|
|
33
|
+
if return_seconds: return duration_seconds
|
|
34
|
+
hours = duration_seconds // 3600
|
|
35
|
+
minutes = (duration_seconds % 3600) // 60
|
|
36
|
+
seconds = duration_seconds % 60
|
|
37
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
'''estimatedurationwithfilelink'''
|
|
41
|
+
def estimatedurationwithfilelink(filelink: str = '', headers: dict = None, request_overrides: dict = None):
|
|
42
|
+
headers, request_overrides = headers or {}, request_overrides or {}
|
|
43
|
+
try:
|
|
44
|
+
resp = requests.get(filelink, headers=headers, timeout=10, **request_overrides)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
f = BytesIO(resp.content)
|
|
47
|
+
audio = MutagenFile(f)
|
|
48
|
+
length = getattr(audio.info, "length", 0)
|
|
49
|
+
return int(length)
|
|
50
|
+
except:
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
'''cookies2dict'''
|
|
55
|
+
def cookies2dict(cookies: str | dict = None):
|
|
56
|
+
if not cookies: cookies = {}
|
|
57
|
+
if isinstance(cookies, dict): return cookies
|
|
58
|
+
if isinstance(cookies, str): return dict(item.split("=", 1) for item in cookies.split("; "))
|
|
59
|
+
raise TypeError(f'cookies type is "{type(cookies)}", expect cookies to "str" or "dict" or "None".')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
'''cookies2string'''
|
|
63
|
+
def cookies2string(cookies: str | dict = None):
|
|
64
|
+
if not cookies: cookies = ""
|
|
65
|
+
if isinstance(cookies, str): return cookies
|
|
66
|
+
if isinstance(cookies, dict): return "; ".join(f"{k}={v}" for k, v in cookies.items())
|
|
67
|
+
raise TypeError(f'cookies type is "{type(cookies)}", expect cookies to "str" or "dict" or "None".')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
'''touchdir'''
|
|
71
|
+
def touchdir(directory, exist_ok=True, mode=511, auto_sanitize=True):
|
|
72
|
+
if auto_sanitize: directory = sanitize_filepath(directory)
|
|
73
|
+
return os.makedirs(directory, exist_ok=exist_ok, mode=mode)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
'''replacefile'''
|
|
77
|
+
def replacefile(src: str, dest: str):
|
|
78
|
+
try:
|
|
79
|
+
os.replace(src, dest)
|
|
80
|
+
except OSError as exc:
|
|
81
|
+
if exc.errno != errno.EXDEV: raise
|
|
82
|
+
if os.path.exists(dest):
|
|
83
|
+
if os.path.isdir(dest): raise
|
|
84
|
+
os.remove(dest)
|
|
85
|
+
shutil.move(src, dest)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
'''legalizestring'''
|
|
89
|
+
def legalizestring(string: str, fit_gbk: bool = True, max_len: int = 255, fit_utf8: bool = True, replace_null_string: str = 'NULL'):
|
|
90
|
+
string = str(string)
|
|
91
|
+
string = string.replace(r'\"', '"')
|
|
92
|
+
string = re.sub(r"<\\/", "</", string)
|
|
93
|
+
string = re.sub(r"\\/>", "/>", string)
|
|
94
|
+
string = re.sub(r"\\u([0-9a-fA-F]{4})", lambda m: chr(int(m.group(1), 16)), string)
|
|
95
|
+
# html.unescape
|
|
96
|
+
for _ in range(2):
|
|
97
|
+
new_string = html.unescape(string)
|
|
98
|
+
if new_string == string: break
|
|
99
|
+
string = new_string
|
|
100
|
+
# bleach.clean
|
|
101
|
+
try:
|
|
102
|
+
string = BeautifulSoup(string, "lxml").get_text(separator="")
|
|
103
|
+
except:
|
|
104
|
+
string = bleach.clean(string, tags=[], attributes={}, strip=True)
|
|
105
|
+
# unicodedata.normalize
|
|
106
|
+
string = unicodedata.normalize("NFC", string)
|
|
107
|
+
# emoji.replace_emoji
|
|
108
|
+
string = emoji.replace_emoji(string, replace="")
|
|
109
|
+
# isprintable
|
|
110
|
+
string = "".join([ch for ch in string if ch.isprintable() and not unicodedata.category(ch).startswith("C")])
|
|
111
|
+
# sanitize_filename
|
|
112
|
+
string = sanitize_filename(string, max_len=max_len)
|
|
113
|
+
# fix encoding
|
|
114
|
+
if fit_gbk: string = string.encode("gbk", errors="ignore").decode("gbk", errors="ignore")
|
|
115
|
+
if fit_utf8: string = string.encode("utf-8", errors="ignore").decode("utf-8", errors="ignore")
|
|
116
|
+
# return
|
|
117
|
+
string = re.sub(r"\s+", " ", string).strip()
|
|
118
|
+
if not string: string = replace_null_string
|
|
119
|
+
return string
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
'''seconds2hms'''
|
|
123
|
+
def seconds2hms(seconds: int):
|
|
124
|
+
try:
|
|
125
|
+
seconds = int(float(seconds))
|
|
126
|
+
m, s = divmod(seconds, 60)
|
|
127
|
+
h, m = divmod(m, 60)
|
|
128
|
+
hms = '%02d:%02d:%02d' % (h, m, s)
|
|
129
|
+
if hms == '00:00:00': hms = '-:-:-'
|
|
130
|
+
except:
|
|
131
|
+
hms = '-:-:-'
|
|
132
|
+
return hms
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
'''byte2mb'''
|
|
136
|
+
def byte2mb(size: int):
|
|
137
|
+
try:
|
|
138
|
+
size = int(float(size))
|
|
139
|
+
if size == 0: return 'NULL'
|
|
140
|
+
size = round(size / 1024 / 1024, 2)
|
|
141
|
+
if size == 0.0: return 'NULL'
|
|
142
|
+
size = f'{size} MB'
|
|
143
|
+
except:
|
|
144
|
+
size = 'NULL'
|
|
145
|
+
return size
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
'''resp2json'''
|
|
149
|
+
def resp2json(resp: requests.Response):
|
|
150
|
+
if not isinstance(resp, requests.Response): return {}
|
|
151
|
+
try:
|
|
152
|
+
result = resp.json()
|
|
153
|
+
except:
|
|
154
|
+
result = json_repair.loads(resp.text)
|
|
155
|
+
if not result: result = dict()
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
'''isvalidresp'''
|
|
160
|
+
def isvalidresp(resp: requests.Response, valid_status_codes: list = [200]):
|
|
161
|
+
if not isinstance(resp, requests.Response): return False
|
|
162
|
+
if resp is None or resp.status_code not in valid_status_codes:
|
|
17
163
|
return False
|
|
18
164
|
return True
|
|
19
165
|
|
|
20
166
|
|
|
21
|
-
'''
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
167
|
+
'''safeextractfromdict'''
|
|
168
|
+
def safeextractfromdict(data, progressive_keys, default_value):
|
|
169
|
+
try:
|
|
170
|
+
result = data
|
|
171
|
+
for key in progressive_keys: result = result[key]
|
|
172
|
+
except:
|
|
173
|
+
result = default_value
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
'''cachecookies'''
|
|
178
|
+
def cachecookies(client_name: str = '', cache_cookie_path: str = '', client_cookies: dict = None):
|
|
179
|
+
if os.path.exists(cache_cookie_path):
|
|
180
|
+
with open(cache_cookie_path, 'rb') as fp:
|
|
181
|
+
cookies = pickle.load(fp)
|
|
182
|
+
else:
|
|
183
|
+
cookies = dict()
|
|
184
|
+
with open(cache_cookie_path, 'wb') as fp:
|
|
185
|
+
cookies[client_name] = client_cookies
|
|
186
|
+
pickle.dump(cookies, fp)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
'''usedownloadheaderscookies'''
|
|
190
|
+
def usedownloadheaderscookies(func):
|
|
191
|
+
@functools.wraps(func)
|
|
192
|
+
def wrapper(self, *args, **kwargs):
|
|
193
|
+
self.default_headers = self.default_download_headers
|
|
194
|
+
self.default_cookies = self.default_download_cookies
|
|
195
|
+
if hasattr(self, '_initsession'): self._initsession()
|
|
196
|
+
return func(self, *args, **kwargs)
|
|
197
|
+
return wrapper
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
'''useparseheaderscookies'''
|
|
201
|
+
def useparseheaderscookies(func):
|
|
202
|
+
@functools.wraps(func)
|
|
203
|
+
def wrapper(self, *args, **kwargs):
|
|
204
|
+
self.default_headers = self.default_parse_headers
|
|
205
|
+
self.default_cookies = self.default_parse_cookies
|
|
206
|
+
if hasattr(self, '_initsession'): self._initsession()
|
|
207
|
+
return func(self, *args, **kwargs)
|
|
208
|
+
return wrapper
|
|
25
209
|
|
|
26
210
|
|
|
27
|
-
'''
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
211
|
+
'''usesearchheaderscookies'''
|
|
212
|
+
def usesearchheaderscookies(func):
|
|
213
|
+
@functools.wraps(func)
|
|
214
|
+
def wrapper(self, *args, **kwargs):
|
|
215
|
+
self.default_headers = self.default_search_headers
|
|
216
|
+
self.default_cookies = self.default_search_cookies
|
|
217
|
+
if hasattr(self, '_initsession'): self._initsession()
|
|
218
|
+
return func(self, *args, **kwargs)
|
|
219
|
+
return wrapper
|
|
33
220
|
|
|
34
221
|
|
|
35
|
-
'''
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
222
|
+
'''AudioLinkTester'''
|
|
223
|
+
class AudioLinkTester(object):
|
|
224
|
+
MAGIC = [
|
|
225
|
+
(b"ID3", "mp3"), (b"\xFF\xFB", "mp3"), (b"fLaC", "flac"), (b"RIFF", "wav"), (b"OggS", "ogg"), (b"MThd", "midi"), (b"\x00\x00\x00\x18ftyp", "mp4/m4a"),
|
|
226
|
+
]
|
|
227
|
+
AUDIO_CT_PREFIX = "audio/"
|
|
228
|
+
AUDIO_CT_EXTRA = {
|
|
229
|
+
"application/octet-stream", "application/x-flac", "application/flac", "application/x-mpegurl"
|
|
230
|
+
}
|
|
231
|
+
CTYPE_TO_EXT = {
|
|
232
|
+
"audio/mpeg": "mp3", "audio/mp3": "mp3", "audio/mp4": "m4a", "audio/x-m4a": "m4a", "audio/aac": "aac", "audio/wav": "wav",
|
|
233
|
+
"audio/x-wav": "wav", "audio/flac": "flac", "audio/x-flac": "flac", "audio/ogg": "ogg", "audio/opus": "opus", "audio/x-aac": "ogg",
|
|
234
|
+
}
|
|
235
|
+
def __init__(self, timeout=(5, 15), headers: dict = None, cookies: dict = None):
|
|
236
|
+
self.session = requests.Session()
|
|
237
|
+
self.timeout = timeout
|
|
238
|
+
self.headers = {
|
|
239
|
+
'Accept': '*/*',
|
|
240
|
+
'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',
|
|
241
|
+
}
|
|
242
|
+
self.headers.update(headers or {})
|
|
243
|
+
self.cookies = cookies or {}
|
|
244
|
+
'''isaudioct'''
|
|
245
|
+
@staticmethod
|
|
246
|
+
def isaudioct(ct: str):
|
|
247
|
+
if not ct:
|
|
248
|
+
return False
|
|
249
|
+
ct = ct.lower().split(";", 1)[0].strip()
|
|
250
|
+
return ct.startswith(AudioLinkTester.AUDIO_CT_PREFIX) or ct in AudioLinkTester.AUDIO_CT_EXTRA
|
|
251
|
+
'''sniffmagic'''
|
|
252
|
+
@staticmethod
|
|
253
|
+
def sniffmagic(b: str):
|
|
254
|
+
for sig, fmt in AudioLinkTester.MAGIC:
|
|
255
|
+
if b.startswith(sig):
|
|
256
|
+
return fmt
|
|
257
|
+
if len(b) >= 2 and b[0] == 0xFF and (b[1] & 0xF0) == 0xF0:
|
|
258
|
+
return "aac/adts"
|
|
259
|
+
return None
|
|
260
|
+
'''probe'''
|
|
261
|
+
def probe(self, url: str, request_overrides: dict = None):
|
|
262
|
+
request_overrides = request_overrides or {}
|
|
263
|
+
if 'headers' not in request_overrides: request_overrides['headers'] = self.headers
|
|
264
|
+
if 'timeout' not in request_overrides: request_overrides['timeout'] = self.timeout
|
|
265
|
+
if 'cookies' not in request_overrides: request_overrides['cookies'] = self.cookies
|
|
266
|
+
outputs = dict(file_size='NULL', ctype='NULL', ext='NULL', download_url=url, final_url='NULL')
|
|
267
|
+
# HEAD probe
|
|
268
|
+
try:
|
|
269
|
+
resp = self.session.head(url, allow_redirects=True, **request_overrides)
|
|
270
|
+
resp.raise_for_status()
|
|
271
|
+
resp_headers, final_url = resp.headers, resp.url
|
|
272
|
+
resp.close()
|
|
273
|
+
file_size, ctype = byte2mb(resp_headers.get('content-length')), resp_headers.get('content-type')
|
|
274
|
+
if ctype == 'image/jpg; charset=UTF-8' or ctype == 'image/jpg':
|
|
275
|
+
ctype = 'audio/mpeg'
|
|
276
|
+
ext = self.CTYPE_TO_EXT.get(ctype, 'NULL')
|
|
277
|
+
outputs = dict(file_size=file_size, ctype=ctype, ext=ext, download_url=url, final_url=final_url)
|
|
278
|
+
except:
|
|
279
|
+
outputs = dict(file_size='NULL', ctype='NULL', ext='NULL', download_url=url, final_url='NULL')
|
|
280
|
+
if outputs['file_size'] not in ['NULL']: return outputs
|
|
281
|
+
# GETSTREAM probe
|
|
282
|
+
try:
|
|
283
|
+
resp = self.session.get(url, allow_redirects=True, stream=True, **request_overrides)
|
|
284
|
+
resp.raise_for_status()
|
|
285
|
+
resp_headers, final_url = resp.headers, resp.url
|
|
286
|
+
resp.close()
|
|
287
|
+
file_size, ctype = byte2mb(resp_headers.get('content-length')), resp_headers.get('content-type')
|
|
288
|
+
if ctype == 'image/jpg; charset=UTF-8' or ctype == 'image/jpg':
|
|
289
|
+
ctype = 'audio/mpeg'
|
|
290
|
+
ext = self.CTYPE_TO_EXT.get(ctype, 'NULL')
|
|
291
|
+
outputs = dict(file_size=file_size, ctype=ctype, ext=ext, download_url=url, final_url=final_url)
|
|
292
|
+
except:
|
|
293
|
+
outputs = dict(file_size='NULL', ctype='NULL', ext='NULL', download_url=url, final_url='NULL')
|
|
294
|
+
return outputs
|
|
295
|
+
'''test'''
|
|
296
|
+
def test(self, url: str, request_overrides: dict = None):
|
|
297
|
+
request_overrides = request_overrides or {}
|
|
298
|
+
if 'headers' not in request_overrides: request_overrides['headers'] = self.headers
|
|
299
|
+
if 'timeout' not in request_overrides: request_overrides['timeout'] = self.timeout
|
|
300
|
+
if 'cookies' not in request_overrides: request_overrides['cookies'] = self.cookies
|
|
301
|
+
outputs = dict(ok=False, status=0, method="", final_url=None, ctype=None, clen=None, range=None, fmt=None, reason="")
|
|
302
|
+
# HEAD test
|
|
303
|
+
try:
|
|
304
|
+
resp = self.session.head(url, allow_redirects=True, **request_overrides)
|
|
305
|
+
clen = resp.headers.get("Content-Length")
|
|
306
|
+
clen = int(clen) if clen and clen.isdigit() else None
|
|
307
|
+
outputs.update(dict(
|
|
308
|
+
status=resp.status_code, method="HEAD", final_url=str(resp.url), ctype=resp.headers.get("Content-Type"),
|
|
309
|
+
clen=clen, range=(resp.headers.get("Accept-Ranges") or "").lower() == "bytes",
|
|
310
|
+
))
|
|
311
|
+
if 200 <= resp.status_code < 300 and (self.isaudioct(outputs["ctype"]) and (outputs["clen"] or outputs["range"])):
|
|
312
|
+
outputs.update(dict(
|
|
313
|
+
ok=True, reason="HEAD success"
|
|
314
|
+
))
|
|
315
|
+
return outputs
|
|
316
|
+
except Exception as err:
|
|
317
|
+
outputs["reason"] = f"HEAD error: {err}"
|
|
318
|
+
# RANGEGET test
|
|
319
|
+
try:
|
|
320
|
+
headers = copy.deepcopy(self.headers)
|
|
321
|
+
headers["Range"] = "bytes=0-15"
|
|
322
|
+
resp = self.session.get(url, stream=True, allow_redirects=True, **request_overrides)
|
|
323
|
+
outputs.update(dict(
|
|
324
|
+
status=resp.status_code, method="RANGEGET", final_url=str(resp.url),
|
|
325
|
+
))
|
|
326
|
+
if resp.status_code not in (200, 206):
|
|
327
|
+
outputs["reason"] = f"RANGEGET error: response status {resp.status_code}"
|
|
328
|
+
return outputs
|
|
329
|
+
chunk = b""
|
|
330
|
+
for b in resp.iter_content(chunk_size=16):
|
|
331
|
+
chunk = b
|
|
332
|
+
break
|
|
333
|
+
resp.close()
|
|
334
|
+
outputs["ctype"] = outputs["ctype"] or resp.headers.get("Content-Type")
|
|
335
|
+
outputs["range"] = outputs["range"] or (resp.status_code == 206) or (resp.headers.get("Content-Range") is not None)
|
|
336
|
+
clen = resp.headers.get("Content-Length") or (resp.headers.get("Content-Range") or "").split("/")[-1]
|
|
337
|
+
if clen and clen.isdigit():
|
|
338
|
+
outputs["clen"] = int(clen)
|
|
339
|
+
outputs["fmt"] = self.sniffmagic(chunk)
|
|
340
|
+
if self.isaudioct(outputs["ctype"]) or outputs["fmt"]:
|
|
341
|
+
outputs.update(dict(
|
|
342
|
+
ok=True, reason="RANGEGET success"
|
|
343
|
+
))
|
|
344
|
+
else:
|
|
345
|
+
outputs.update(dict(
|
|
346
|
+
ok=False, reason="RANGEGET error: Not audio-like (CT/magic)"
|
|
347
|
+
))
|
|
348
|
+
except Exception as err:
|
|
349
|
+
outputs["reason"] = f"RANGEGET error: {err}"
|
|
350
|
+
# return
|
|
351
|
+
return outputs
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of BaseModuleBuilder
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import copy
|
|
10
|
+
import collections
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
'''BaseModuleBuilder'''
|
|
14
|
+
class BaseModuleBuilder():
|
|
15
|
+
REGISTERED_MODULES = collections.OrderedDict()
|
|
16
|
+
def __init__(self, requires_register_modules=None, requires_renew_modules=None):
|
|
17
|
+
if requires_register_modules is not None and isinstance(requires_register_modules, (dict, collections.OrderedDict)):
|
|
18
|
+
for name, module in requires_register_modules.items():
|
|
19
|
+
self.register(name, module)
|
|
20
|
+
if requires_renew_modules is not None and isinstance(requires_renew_modules, (dict, collections.OrderedDict)):
|
|
21
|
+
for name, module in requires_renew_modules.items():
|
|
22
|
+
self.renew(name, module)
|
|
23
|
+
self.validate()
|
|
24
|
+
'''build'''
|
|
25
|
+
def build(self, module_cfg):
|
|
26
|
+
module_cfg = copy.deepcopy(module_cfg)
|
|
27
|
+
module_type = module_cfg.pop('type')
|
|
28
|
+
module = self.REGISTERED_MODULES[module_type](**module_cfg)
|
|
29
|
+
return module
|
|
30
|
+
'''register'''
|
|
31
|
+
def register(self, name, module):
|
|
32
|
+
assert callable(module)
|
|
33
|
+
assert name not in self.REGISTERED_MODULES
|
|
34
|
+
self.REGISTERED_MODULES[name] = module
|
|
35
|
+
'''renew'''
|
|
36
|
+
def renew(self, name, module):
|
|
37
|
+
assert callable(module)
|
|
38
|
+
assert name in self.REGISTERED_MODULES
|
|
39
|
+
self.REGISTERED_MODULES[name] = module
|
|
40
|
+
'''validate'''
|
|
41
|
+
def validate(self):
|
|
42
|
+
for _, module in self.REGISTERED_MODULES.items():
|
|
43
|
+
assert callable(module)
|
|
44
|
+
'''delete'''
|
|
45
|
+
def delete(self, name):
|
|
46
|
+
assert name in self.REGISTERED_MODULES
|
|
47
|
+
del self.REGISTERED_MODULES[name]
|
|
48
|
+
'''pop'''
|
|
49
|
+
def pop(self, name):
|
|
50
|
+
assert name in self.REGISTERED_MODULES
|
|
51
|
+
module = self.REGISTERED_MODULES.pop(name)
|
|
52
|
+
return module
|
|
53
|
+
'''get'''
|
|
54
|
+
def get(self, name):
|
|
55
|
+
assert name in self.REGISTERED_MODULES
|
|
56
|
+
module = self.REGISTERED_MODULES.get(name)
|
|
57
|
+
return module
|
|
58
|
+
'''items'''
|
|
59
|
+
def items(self):
|
|
60
|
+
return self.REGISTERED_MODULES.items()
|
|
61
|
+
'''clear'''
|
|
62
|
+
def clear(self):
|
|
63
|
+
return self.REGISTERED_MODULES.clear()
|
|
64
|
+
'''values'''
|
|
65
|
+
def values(self):
|
|
66
|
+
return self.REGISTERED_MODULES.values()
|
|
67
|
+
'''keys'''
|
|
68
|
+
def keys(self):
|
|
69
|
+
return self.REGISTERED_MODULES.keys()
|
|
70
|
+
'''copy'''
|
|
71
|
+
def copy(self):
|
|
72
|
+
return self.REGISTERED_MODULES.copy()
|
|
73
|
+
'''update'''
|
|
74
|
+
def update(self, requires_update_modules):
|
|
75
|
+
return self.REGISTERED_MODULES.update(requires_update_modules)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of NeteaseMusicClient utils
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import base64
|
|
12
|
+
import urllib
|
|
13
|
+
import codecs
|
|
14
|
+
import urllib.parse
|
|
15
|
+
from hashlib import md5
|
|
16
|
+
from Crypto.Cipher import AES
|
|
17
|
+
from cryptography.hazmat.primitives import padding
|
|
18
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
'''EapiCryptoUtils'''
|
|
22
|
+
class EapiCryptoUtils(object):
|
|
23
|
+
'''hexdigest'''
|
|
24
|
+
@staticmethod
|
|
25
|
+
def hexdigest(data: bytes):
|
|
26
|
+
return "".join([hex(d)[2:].zfill(2) for d in data])
|
|
27
|
+
'''hashdigest'''
|
|
28
|
+
@staticmethod
|
|
29
|
+
def hashdigest(text: str):
|
|
30
|
+
return md5(text.encode("utf-8")).digest()
|
|
31
|
+
'''hashhexdigest'''
|
|
32
|
+
@staticmethod
|
|
33
|
+
def hashhexdigest(text: str):
|
|
34
|
+
return EapiCryptoUtils.hexdigest(EapiCryptoUtils.hashdigest(text))
|
|
35
|
+
'''encryptparams'''
|
|
36
|
+
@staticmethod
|
|
37
|
+
def encryptparams(url: str, payload: dict, aes_key: bytes = b"e82ckenh8dichen8"):
|
|
38
|
+
url_path = urllib.parse.urlparse(url).path.replace("/eapi/", "/api/")
|
|
39
|
+
digest = EapiCryptoUtils.hashhexdigest(f"nobody{url_path}use{json.dumps(payload)}md5forencrypt")
|
|
40
|
+
params = f"{url_path}-36cd479b6b5-{json.dumps(payload)}-36cd479b6b5-{digest}"
|
|
41
|
+
padder = padding.PKCS7(algorithms.AES(aes_key).block_size).padder()
|
|
42
|
+
padded_data = padder.update(params.encode()) + padder.finalize()
|
|
43
|
+
cipher = Cipher(algorithms.AES(aes_key), modes.ECB())
|
|
44
|
+
encryptor = cipher.encryptor()
|
|
45
|
+
enc = encryptor.update(padded_data) + encryptor.finalize()
|
|
46
|
+
return EapiCryptoUtils.hexdigest(enc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
'''WeapiCryptoUtils'''
|
|
50
|
+
class WeapiCryptoUtils(object):
|
|
51
|
+
'''createsecretkey'''
|
|
52
|
+
@staticmethod
|
|
53
|
+
def createsecretkey(size: int):
|
|
54
|
+
return (''.join(map(lambda xx: (hex(ord(xx))[2:]), str(os.urandom(size)))))[0: 16]
|
|
55
|
+
'''aesencrypt'''
|
|
56
|
+
@staticmethod
|
|
57
|
+
def aesencrypt(string: str, sec_key: str):
|
|
58
|
+
pad = 16 - len(string) % 16
|
|
59
|
+
if isinstance(string, bytes): string = string.decode('utf-8')
|
|
60
|
+
string = string + str(pad * chr(pad))
|
|
61
|
+
sec_key = sec_key.encode('utf-8')
|
|
62
|
+
encryptor = AES.new(sec_key, 2, b'0102030405060708')
|
|
63
|
+
string = string.encode('utf-8')
|
|
64
|
+
ciphertext = encryptor.encrypt(string)
|
|
65
|
+
ciphertext = base64.b64encode(ciphertext)
|
|
66
|
+
return ciphertext
|
|
67
|
+
'''rsaencrypt'''
|
|
68
|
+
@staticmethod
|
|
69
|
+
def rsaencrypt(string: str, pub_key: str = '010001', modulus: str = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'):
|
|
70
|
+
string = string[::-1]
|
|
71
|
+
rs = int(codecs.encode(string.encode('utf-8'), 'hex_codec'), 16) ** int(pub_key, 16) % int(modulus, 16)
|
|
72
|
+
return format(rs, 'x').zfill(256)
|
|
73
|
+
'''encryptparams'''
|
|
74
|
+
@staticmethod
|
|
75
|
+
def encryptparams(params: dict):
|
|
76
|
+
string = json.dumps(params)
|
|
77
|
+
sec_key = WeapiCryptoUtils.createsecretkey(16)
|
|
78
|
+
enc_string = WeapiCryptoUtils.aesencrypt(string=WeapiCryptoUtils.aesencrypt(string=string, sec_key='0CoJUm6Qyw8W8jud'), sec_key=sec_key)
|
|
79
|
+
enc_sec_key = WeapiCryptoUtils.rsaencrypt(string=sec_key)
|
|
80
|
+
post_data = {'params': enc_string, 'encSecKey': enc_sec_key}
|
|
81
|
+
return post_data
|