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.
Files changed (59) hide show
  1. musicdl/__init__.py +5 -5
  2. musicdl/modules/__init__.py +10 -3
  3. musicdl/modules/common/__init__.py +2 -0
  4. musicdl/modules/common/gdstudio.py +204 -0
  5. musicdl/modules/js/__init__.py +1 -0
  6. musicdl/modules/js/youtube/__init__.py +2 -0
  7. musicdl/modules/js/youtube/botguard.js +1 -0
  8. musicdl/modules/js/youtube/jsinterp.py +902 -0
  9. musicdl/modules/js/youtube/runner.js +2 -0
  10. musicdl/modules/sources/__init__.py +41 -10
  11. musicdl/modules/sources/apple.py +207 -0
  12. musicdl/modules/sources/base.py +256 -28
  13. musicdl/modules/sources/bilibili.py +118 -0
  14. musicdl/modules/sources/buguyy.py +148 -0
  15. musicdl/modules/sources/fangpi.py +153 -0
  16. musicdl/modules/sources/fivesing.py +108 -0
  17. musicdl/modules/sources/gequbao.py +148 -0
  18. musicdl/modules/sources/jamendo.py +108 -0
  19. musicdl/modules/sources/joox.py +104 -68
  20. musicdl/modules/sources/kugou.py +129 -76
  21. musicdl/modules/sources/kuwo.py +188 -68
  22. musicdl/modules/sources/lizhi.py +107 -0
  23. musicdl/modules/sources/migu.py +172 -66
  24. musicdl/modules/sources/mitu.py +140 -0
  25. musicdl/modules/sources/mp3juice.py +264 -0
  26. musicdl/modules/sources/netease.py +163 -115
  27. musicdl/modules/sources/qianqian.py +125 -77
  28. musicdl/modules/sources/qq.py +232 -94
  29. musicdl/modules/sources/tidal.py +342 -0
  30. musicdl/modules/sources/ximalaya.py +256 -0
  31. musicdl/modules/sources/yinyuedao.py +144 -0
  32. musicdl/modules/sources/youtube.py +238 -0
  33. musicdl/modules/utils/__init__.py +12 -4
  34. musicdl/modules/utils/appleutils.py +563 -0
  35. musicdl/modules/utils/data.py +107 -0
  36. musicdl/modules/utils/logger.py +211 -58
  37. musicdl/modules/utils/lyric.py +73 -0
  38. musicdl/modules/utils/misc.py +335 -23
  39. musicdl/modules/utils/modulebuilder.py +75 -0
  40. musicdl/modules/utils/neteaseutils.py +81 -0
  41. musicdl/modules/utils/qqutils.py +184 -0
  42. musicdl/modules/utils/quarkparser.py +105 -0
  43. musicdl/modules/utils/songinfoutils.py +54 -0
  44. musicdl/modules/utils/tidalutils.py +738 -0
  45. musicdl/modules/utils/youtubeutils.py +3606 -0
  46. musicdl/musicdl.py +184 -86
  47. musicdl-2.7.3.dist-info/LICENSE +203 -0
  48. musicdl-2.7.3.dist-info/METADATA +704 -0
  49. musicdl-2.7.3.dist-info/RECORD +53 -0
  50. {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/WHEEL +5 -5
  51. musicdl-2.7.3.dist-info/entry_points.txt +2 -0
  52. musicdl/modules/sources/baiduFlac.py +0 -69
  53. musicdl/modules/sources/xiami.py +0 -104
  54. musicdl/modules/utils/downloader.py +0 -80
  55. musicdl-2.1.11.dist-info/LICENSE +0 -22
  56. musicdl-2.1.11.dist-info/METADATA +0 -82
  57. musicdl-2.1.11.dist-info/RECORD +0 -24
  58. {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/top_level.txt +0 -0
  59. {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/zip-safe +0 -0
@@ -1,39 +1,351 @@
1
1
  '''
2
2
  Function:
3
- 一些工具函数
3
+ Implementation of common utils
4
4
  Author:
5
- Charles
6
- 微信公众号:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
7
  Charles的皮卡丘
8
8
  '''
9
+ import re
9
10
  import os
10
- import json
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 checkDir(dirpath):
15
- if not os.path.exists(dirpath):
16
- os.mkdir(dirpath)
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 loadConfig(filepath='config.json'):
23
- f = open(filepath, 'r', encoding='utf-8')
24
- return json.load(f)
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 filterBadCharacter(string):
29
- need_removed_strs = ['<em>', '</em>', '<', '>', '\\', '/', '?', ':', '"', ':', '|', '?', '*']
30
- for item in need_removed_strs:
31
- string = string.replace(item, '')
32
- return string.strip().encode('utf-8', 'ignore').decode('utf-8')
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
- def seconds2hms(seconds):
37
- m, s = divmod(seconds, 60)
38
- h, m = divmod(m, 60)
39
- return '%02d:%02d:%02d' % (h, m, s)
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