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,124 +1,172 @@
1
1
  '''
2
2
  Function:
3
- 网易云音乐下载: https://music.163.com/
3
+ Implementation of NeteaseMusicClient: https://music.163.com/
4
4
  Author:
5
- Charles
6
- 微信公众号:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
7
  Charles的皮卡丘
8
8
  '''
9
- import os
10
- import base64
11
- import codecs
12
- import requests
13
- from .base import Base
14
- from ..utils.misc import *
15
- from Crypto.Cipher import AES
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
- Function:
20
- 用于算post的两个参数, 具体原理详见知乎:
21
- https://www.zhihu.com/question/36081767
22
- '''
23
- class Cracker():
24
- def __init__(self):
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.search_url = 'http://music.163.com/weapi/cloudsearch/get/web?csrf_token='
124
- self.player_url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token='
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
- 千千音乐下载: http://music.taihe.com/
3
+ Implementation of QianqianMusicClient: http://music.taihe.com/
4
4
  Author:
5
- Charles
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 requests
12
- from .base import Base
13
- from ..utils.misc import *
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 qianqian(Base):
18
- def __init__(self, config, logger_handle, **kwargs):
19
- super(qianqian, self).__init__(config, logger_handle, **kwargs)
20
- self.source = 'qianqian'
21
- self.__initialize()
22
- '''歌曲搜索'''
23
- def search(self, keyword):
24
- self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword))
25
- cfg = self.config.copy()
26
- params = {
27
- 'sign': self.__calcSign(keyword),
28
- 'word': keyword,
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
- response = self.session.get(self.search_url, headers=self.headers, params=params)
32
- all_items = response.json()['data']['typeTrack']
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.search_url = 'https://music.taihe.com/v1/search'
88
- self.tracklink_url = 'https://music.taihe.com/v1/song/tracklink'
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