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/sources/kugou.py
CHANGED
|
@@ -1,85 +1,138 @@
|
|
|
1
1
|
'''
|
|
2
2
|
Function:
|
|
3
|
-
|
|
3
|
+
Implementation of KugouMusicClient: http://www.kugou.com/
|
|
4
4
|
Author:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
7
|
Charles的皮卡丘
|
|
8
8
|
'''
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
from .base import
|
|
12
|
-
from
|
|
9
|
+
import copy
|
|
10
|
+
import base64
|
|
11
|
+
from .base import BaseMusicClient
|
|
12
|
+
from urllib.parse import urlencode
|
|
13
|
+
from rich.progress import Progress
|
|
14
|
+
from ..utils import legalizestring, byte2mb, resp2json, seconds2hms, usesearchheaderscookies, safeextractfromdict, SongInfo
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
'''
|
|
16
|
-
class
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.
|
|
20
|
-
self.
|
|
21
|
-
|
|
22
|
-
def search(self, keyword):
|
|
23
|
-
self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword))
|
|
24
|
-
cfg = self.config.copy()
|
|
25
|
-
params = {
|
|
26
|
-
'keyword': keyword,
|
|
27
|
-
'page': '1',
|
|
28
|
-
'pagesize': cfg['search_size_per_source'],
|
|
29
|
-
'userid': '-1',
|
|
30
|
-
'clientver': '',
|
|
31
|
-
'platform': 'WebFilter',
|
|
32
|
-
'tag': 'em',
|
|
33
|
-
'filter': '',
|
|
34
|
-
'iscorrection': '1',
|
|
35
|
-
'privilege_filter': '0',
|
|
36
|
-
'_': str(int(time.time() * 1000))
|
|
17
|
+
'''KugouMusicClient'''
|
|
18
|
+
class KugouMusicClient(BaseMusicClient):
|
|
19
|
+
source = 'KugouMusicClient'
|
|
20
|
+
def __init__(self, **kwargs):
|
|
21
|
+
super(KugouMusicClient, self).__init__(**kwargs)
|
|
22
|
+
self.default_search_headers = {
|
|
23
|
+
'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',
|
|
37
24
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
songinfos = []
|
|
41
|
-
for item in all_items:
|
|
42
|
-
params = {
|
|
43
|
-
'r': 'play/getdata',
|
|
44
|
-
'hash': str(item['FileHash']),
|
|
45
|
-
'album_id': str(item['AlbumID']),
|
|
46
|
-
'dfid': '1aAcF31Utj2l0ZzFPO0Yjss0',
|
|
47
|
-
'mid': 'ccbb9592c3177be2f3977ff292e0f145',
|
|
48
|
-
'platid': '4',
|
|
49
|
-
'_': str(int(time.time() * 1000))
|
|
50
|
-
}
|
|
51
|
-
response = self.session.get(self.hash_url, headers=self.hash_headers, params=params)
|
|
52
|
-
response_json = response.json()
|
|
53
|
-
if response_json.get('err_code') != 0: continue
|
|
54
|
-
download_url = response_json['data']['play_url'].replace('\\', '')
|
|
55
|
-
if not download_url: continue
|
|
56
|
-
filesize = str(round(int(response_json['data']['filesize'])/1024/1024, 2)) + 'MB'
|
|
57
|
-
ext = download_url.split('.')[-1]
|
|
58
|
-
duration = int(item.get('Duration', 0))
|
|
59
|
-
songinfo = {
|
|
60
|
-
'source': self.source,
|
|
61
|
-
'songid': str(item['ID']),
|
|
62
|
-
'singers': filterBadCharacter(item.get('SingerName', '-')),
|
|
63
|
-
'album': filterBadCharacter(item.get('AlbumName', '-')),
|
|
64
|
-
'songname': filterBadCharacter(item.get('SongName', '-')),
|
|
65
|
-
'savedir': cfg['savedir'],
|
|
66
|
-
'savename': '_'.join([self.source, filterBadCharacter(item.get('SongName', '-'))]),
|
|
67
|
-
'download_url': download_url,
|
|
68
|
-
'filesize': filesize,
|
|
69
|
-
'ext': ext,
|
|
70
|
-
'duration': seconds2hms(duration)
|
|
71
|
-
}
|
|
72
|
-
songinfos.append(songinfo)
|
|
73
|
-
return songinfos
|
|
74
|
-
'''初始化'''
|
|
75
|
-
def __initialize(self):
|
|
76
|
-
self.search_headers = {
|
|
77
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36',
|
|
78
|
-
'Referer': 'https://www.kugou.com/yy/html/search.html'
|
|
25
|
+
self.default_download_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',
|
|
79
27
|
}
|
|
80
|
-
self.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
28
|
+
self.default_headers = self.default_search_headers
|
|
29
|
+
self._initsession()
|
|
30
|
+
'''_constructsearchurls'''
|
|
31
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
32
|
+
# init
|
|
33
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
34
|
+
# search rules
|
|
35
|
+
default_rule = {'keyword': keyword, 'page': 1, 'pagesize': 10}
|
|
36
|
+
default_rule.update(rule)
|
|
37
|
+
# construct search urls based on search rules
|
|
38
|
+
base_url = 'http://songsearch.kugou.com/song_search_v2?'
|
|
39
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
40
|
+
while self.search_size_per_source > count:
|
|
41
|
+
page_rule = copy.deepcopy(default_rule)
|
|
42
|
+
page_rule['pagesize'] = page_size
|
|
43
|
+
page_rule['page'] = int(count // page_size) + 1
|
|
44
|
+
search_urls.append(base_url + urlencode(page_rule))
|
|
45
|
+
count += page_size
|
|
46
|
+
# return
|
|
47
|
+
return search_urls
|
|
48
|
+
'''_search'''
|
|
49
|
+
@usesearchheaderscookies
|
|
50
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
51
|
+
# init
|
|
52
|
+
request_overrides = request_overrides or {}
|
|
53
|
+
# successful
|
|
54
|
+
try:
|
|
55
|
+
# --search results
|
|
56
|
+
resp = self.get(search_url, **request_overrides)
|
|
57
|
+
resp.raise_for_status()
|
|
58
|
+
search_results = resp2json(resp)['data']['lists']
|
|
59
|
+
for search_result in search_results:
|
|
60
|
+
# --download results
|
|
61
|
+
if not isinstance(search_result, dict) or ('FileHash' not in search_result):
|
|
62
|
+
continue
|
|
63
|
+
song_info = SongInfo(source=self.source)
|
|
64
|
+
try:
|
|
65
|
+
resp = self.get(f"http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={search_result['FileHash']}", **request_overrides)
|
|
66
|
+
resp.raise_for_status()
|
|
67
|
+
download_result_default: dict = resp2json(resp)
|
|
68
|
+
except:
|
|
69
|
+
continue
|
|
70
|
+
better_hashes = [
|
|
71
|
+
safeextractfromdict(download_result_default, ['extra', 'highhash'], ""), safeextractfromdict(download_result_default, ['extra', 'sqhash'], ""),
|
|
72
|
+
safeextractfromdict(download_result_default, ['extra', '320hash'], ""), safeextractfromdict(download_result_default, ['extra', '128hash'], ""),
|
|
73
|
+
]
|
|
74
|
+
for better_hash in better_hashes:
|
|
75
|
+
if not better_hash: continue
|
|
76
|
+
if better_hash == search_result['FileHash']:
|
|
77
|
+
download_result = download_result_default
|
|
78
|
+
download_url = download_result.get('url') or download_result.get('backup_url')
|
|
79
|
+
if not download_url: continue
|
|
80
|
+
if isinstance(download_url, list): download_url = download_url[0]
|
|
81
|
+
song_info = SongInfo(
|
|
82
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
83
|
+
)
|
|
84
|
+
if song_info.with_valid_download_url: break
|
|
85
|
+
else:
|
|
86
|
+
try:
|
|
87
|
+
resp = self.get(f"http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={better_hash}", **request_overrides)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
download_result: dict = resp2json(resp)
|
|
90
|
+
download_url = download_result.get('url') or download_result.get('backup_url')
|
|
91
|
+
if not download_url: continue
|
|
92
|
+
if isinstance(download_url, list): download_url = download_url[0]
|
|
93
|
+
song_info = SongInfo(
|
|
94
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
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.update(dict(
|
|
101
|
+
file_size_bytes=download_result.get('fileSize', 0), file_size=byte2mb(download_result.get('fileSize', 0)),
|
|
102
|
+
duration_s=download_result.get('timeLength', 0), duration=seconds2hms(download_result.get('timeLength', 0)),
|
|
103
|
+
raw_data={'search': search_result, 'download': download_result}, ext=download_result.get('extName', 'mp3'),
|
|
104
|
+
song_name=legalizestring(search_result.get('SongName', 'NULL'), replace_null_string='NULL'),
|
|
105
|
+
singers=legalizestring(search_result.get('SingerName', 'NULL'), replace_null_string='NULL'),
|
|
106
|
+
album=legalizestring(search_result.get('AlbumName', 'NULL'), replace_null_string='NULL'),
|
|
107
|
+
identifier=better_hash,
|
|
108
|
+
))
|
|
109
|
+
if song_info.song_name == 'NULL': song_info.song_name = legalizestring(search_result.get('FileName', 'NULL'), replace_null_string='NULL')
|
|
110
|
+
if song_info.song_name == 'NULL': song_info.song_name = legalizestring(search_result.get('OriSongName', 'NULL'), replace_null_string='NULL')
|
|
111
|
+
# --lyric results
|
|
112
|
+
params = {'keyword': search_result.get('FileName', ''), 'duration': search_result.get('Duration', '99999'), 'hash': better_hash}
|
|
113
|
+
try:
|
|
114
|
+
resp = self.get('http://lyrics.kugou.com/search', params=params, **request_overrides)
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
lyric_result = resp2json(resp=resp)
|
|
117
|
+
id = lyric_result['candidates'][0]['id']
|
|
118
|
+
accesskey = lyric_result['candidates'][0]['accesskey']
|
|
119
|
+
resp = self.get(f'http://lyrics.kugou.com/download?ver=1&client=pc&id={id}&accesskey={accesskey}&fmt=lrc&charset=utf8', **request_overrides)
|
|
120
|
+
resp.raise_for_status()
|
|
121
|
+
lyric_result['lyrics.kugou.com/download'] = resp2json(resp=resp)
|
|
122
|
+
lyric = lyric_result['lyrics.kugou.com/download']['content']
|
|
123
|
+
lyric = base64.b64decode(lyric).decode('utf-8')
|
|
124
|
+
except:
|
|
125
|
+
lyric_result, lyric = dict(), 'NULL'
|
|
126
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
127
|
+
song_info.lyric = lyric
|
|
128
|
+
# --append to song_infos
|
|
129
|
+
song_infos.append(song_info)
|
|
130
|
+
# --judgement for search_size
|
|
131
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
132
|
+
# --update progress
|
|
133
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
134
|
+
# failure
|
|
135
|
+
except Exception as err:
|
|
136
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
137
|
+
# return
|
|
138
|
+
return song_infos
|
musicdl/modules/sources/kuwo.py
CHANGED
|
@@ -1,78 +1,198 @@
|
|
|
1
1
|
'''
|
|
2
2
|
Function:
|
|
3
|
-
|
|
3
|
+
Implementation of KuwoMusicClient: http://www.kuwo.cn/
|
|
4
4
|
Author:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
7
|
Charles的皮卡丘
|
|
8
8
|
'''
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
from .
|
|
12
|
-
from
|
|
9
|
+
import copy
|
|
10
|
+
from .base import BaseMusicClient
|
|
11
|
+
from urllib.parse import urlencode
|
|
12
|
+
from rich.progress import Progress
|
|
13
|
+
from ..utils import legalizestring, resp2json, seconds2hms, usesearchheaderscookies, SongInfo
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
'''
|
|
16
|
-
class
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.
|
|
20
|
-
self.
|
|
21
|
-
|
|
22
|
-
def search(self, keyword):
|
|
23
|
-
self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword))
|
|
24
|
-
cfg = self.config.copy()
|
|
25
|
-
params = {
|
|
26
|
-
'key': keyword,
|
|
27
|
-
'pn': '1',
|
|
28
|
-
'rn': cfg['search_size_per_source'],
|
|
29
|
-
'reqId': 'ffa3dc80-73c2-11ea-a715-7de8a8cc7b68'
|
|
16
|
+
'''KuwoMusicClient'''
|
|
17
|
+
class KuwoMusicClient(BaseMusicClient):
|
|
18
|
+
source = 'KuwoMusicClient'
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super(KuwoMusicClient, self).__init__(**kwargs)
|
|
21
|
+
self.default_search_headers = {
|
|
22
|
+
'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',
|
|
30
23
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
24
|
+
self.default_download_headers = {
|
|
25
|
+
'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',
|
|
26
|
+
}
|
|
27
|
+
self.default_headers = self.default_search_headers
|
|
28
|
+
self._initsession()
|
|
29
|
+
'''_parsewithflacmusicapi'''
|
|
30
|
+
def _parsewithflacmusicapi(self, search_result: dict, request_overrides: dict = None):
|
|
31
|
+
# init
|
|
32
|
+
request_overrides, song_id = request_overrides or {}, search_result['MUSICRID'].removeprefix('MUSIC_')
|
|
33
|
+
headers = copy.deepcopy(self.default_search_headers)
|
|
34
|
+
headers['origin'] = "https://flac.music.hi.cn"
|
|
35
|
+
headers['cookie'] = 'sl-session=Arb4XF1mQmlUkvJAhAps2g==; sl-challenge-server=cloud; sl_jwt_session=Ob+9V4oyQWkappXP5u8Trw==; sl_jwt_sign='
|
|
36
|
+
# parse
|
|
37
|
+
for quality in [('flac', '2000', '2000kflac'), ('mp3', '320', '320kmp3')]:
|
|
38
|
+
try:
|
|
39
|
+
data = {'platform': 'kuwo', 'songid': song_id, 'format': quality[0], 'bitrate': quality[1]}
|
|
40
|
+
resp = self.post('https://flac.music.hi.cn/ajax.php?act=getUrl', headers=headers, data=data, timeout=10, **request_overrides)
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
download_result = resp2json(resp=resp)
|
|
43
|
+
if 'data' not in download_result: continue
|
|
44
|
+
except:
|
|
45
|
+
continue
|
|
46
|
+
download_url: str = download_result['data'].get('url', '')
|
|
49
47
|
if not download_url: continue
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'csrf': '0HQ0UGKNAKR',
|
|
73
|
-
'Host': 'www.kuwo.cn',
|
|
74
|
-
'Referer': 'http://www.kuwo.cn/search/list',
|
|
75
|
-
'Cookie': 'kw_token=0HQ0UGKNAKR;'
|
|
48
|
+
song_info = SongInfo(
|
|
49
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
50
|
+
ext=download_url.split('.')[-1].split('?')[0], raw_data={'search': search_result, 'download': download_result},
|
|
51
|
+
duration_s=download_result['data'].get('duration', 0), duration=seconds2hms(download_result['data'].get('duration', 0)),
|
|
52
|
+
)
|
|
53
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
54
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
55
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
56
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
57
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
58
|
+
if song_info.with_valid_download_url: break
|
|
59
|
+
# return
|
|
60
|
+
return song_info, quality
|
|
61
|
+
'''_constructsearchurls'''
|
|
62
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
63
|
+
# init
|
|
64
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
65
|
+
# search rules
|
|
66
|
+
default_rule = {
|
|
67
|
+
"vipver": "1", "client": "kt", "ft": "music", "cluster": "0", "strategy": "2012", "encoding": "utf8",
|
|
68
|
+
"rformat": "json", "mobi": "1", "issubtitle": "1", "show_copyright_off": "1", "pn": "0", "rn": "10",
|
|
69
|
+
"all": keyword,
|
|
76
70
|
}
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
default_rule.update(rule)
|
|
72
|
+
# construct search urls based on search rules
|
|
73
|
+
base_url = 'http://www.kuwo.cn/search/searchMusicBykeyWord?'
|
|
74
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
75
|
+
while self.search_size_per_source > count:
|
|
76
|
+
page_rule = copy.deepcopy(default_rule)
|
|
77
|
+
page_rule['rn'] = page_size
|
|
78
|
+
page_rule['pn'] = str(int(count // page_size))
|
|
79
|
+
search_urls.append(base_url + urlencode(page_rule))
|
|
80
|
+
count += page_size
|
|
81
|
+
# return
|
|
82
|
+
return search_urls
|
|
83
|
+
'''_search'''
|
|
84
|
+
@usesearchheaderscookies
|
|
85
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
86
|
+
# init
|
|
87
|
+
request_overrides = request_overrides or {}
|
|
88
|
+
# successful
|
|
89
|
+
try:
|
|
90
|
+
# --search results
|
|
91
|
+
resp = self.get(search_url, **request_overrides)
|
|
92
|
+
resp.raise_for_status()
|
|
93
|
+
search_results = resp2json(resp)['abslist']
|
|
94
|
+
for search_result in search_results:
|
|
95
|
+
# --download results
|
|
96
|
+
if not isinstance(search_result, dict) or ('MUSICRID' not in search_result):
|
|
97
|
+
continue
|
|
98
|
+
song_info = SongInfo(source=self.source)
|
|
99
|
+
brs = ['4000kflac', '2000kflac', 'flac', '320kmp3', '192kmp3', '128kmp3']
|
|
100
|
+
# ----try _parsewithflacmusicapi first
|
|
101
|
+
try:
|
|
102
|
+
song_info_flac, quality_flac = self._parsewithflacmusicapi(search_result, request_overrides)
|
|
103
|
+
except:
|
|
104
|
+
song_info_flac, quality_flac = SongInfo(source=self.source), ('mp3', '128', '128kmp3')
|
|
105
|
+
# ----try "https://mobi.kuwo.cn/mobi.s" second
|
|
106
|
+
for br_idx, br in enumerate(brs):
|
|
107
|
+
if song_info_flac.with_valid_download_url and br_idx >= brs.index(quality_flac[-1]): song_info = song_info_flac; break
|
|
108
|
+
try:
|
|
109
|
+
resp = self.get(f"https://mobi.kuwo.cn/mobi.s?f=web&source=kwplayercar_ar_6.0.0.9_B_jiakong_vh.apk&from=PC&type=convert_url_with_sign&br={br}&rid={search_result['MUSICRID'].removeprefix('MUSIC_')}&&user=C_APK_guanwang_12609069939969033731", **request_overrides)
|
|
110
|
+
resp.raise_for_status()
|
|
111
|
+
download_result = resp2json(resp=resp)
|
|
112
|
+
download_url = download_result['data']['url']
|
|
113
|
+
song_info = SongInfo(
|
|
114
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
115
|
+
ext=download_result['data'].get('format', br[-4:].removeprefix('k')), duration_s=download_result['data'].get('duration', 0),
|
|
116
|
+
duration=seconds2hms(download_result['data'].get('duration', 0)), raw_data={'search': search_result, 'download': download_result},
|
|
117
|
+
)
|
|
118
|
+
if song_info.with_valid_download_url: break
|
|
119
|
+
except:
|
|
120
|
+
continue
|
|
121
|
+
# ----try "https://www.kuwo.cn/api/v1/www/music/playUrl", third
|
|
122
|
+
if not song_info.with_valid_download_url:
|
|
123
|
+
headers = {
|
|
124
|
+
"Cookie": (
|
|
125
|
+
"Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1747998937; HMACCOUNT=3E88140C4BD6BF25; _ga=GA1.2.2122710619.1747998937; _gid=GA1.2.1827944406.1747998937; "
|
|
126
|
+
"gtoken=RNbrzHWRp6DY; gid=d55a4884-42aa-4733-98eb-e7aaffc6122e; JSESSIONID=us1icx6617iy1k1ksiuykje71; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1748000521; "
|
|
127
|
+
"_gat=1; _ga_ETPBRPM9ML=GS2.2.s1747998937$o1$g1$t1748000535$j45$l0$h0; Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324=jbikFazGJzBjt2bhSJGMxGfkM5zNYcis"
|
|
128
|
+
),
|
|
129
|
+
"secret": "4932e2c95746126c945fe2fb3f88d3455b85b69a4fbdfa6c44b501d7dfe50cff04eb9a8e",
|
|
130
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0",
|
|
131
|
+
} # TODO: implement secret generate algorithm
|
|
132
|
+
for br in brs:
|
|
133
|
+
params = {'mid': search_result['MUSICRID'].removeprefix('MUSIC_'), 'type': 'music', 'httpsStatus': '1', 'br': br}
|
|
134
|
+
try:
|
|
135
|
+
resp = self.get('https://www.kuwo.cn/api/v1/www/music/playUrl', params=params, headers=headers, **request_overrides)
|
|
136
|
+
resp.raise_for_status()
|
|
137
|
+
download_result = resp2json(resp=resp)
|
|
138
|
+
download_url = download_result['data']['url']
|
|
139
|
+
ext = download_url.split('.')[-1].split('?')[0] or 'mp3'
|
|
140
|
+
song_info = SongInfo(
|
|
141
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
142
|
+
ext=ext, duration_s=search_result.get('DURATION', 0), duration=seconds2hms(search_result.get('DURATION', 0)),
|
|
143
|
+
raw_data={'search': search_result, 'download': download_result},
|
|
144
|
+
)
|
|
145
|
+
if song_info.with_valid_download_url: break
|
|
146
|
+
except:
|
|
147
|
+
continue
|
|
148
|
+
# ----try "http://antiserver.kuwo.cn/anti.s" finally (br only up to 320kmp3)
|
|
149
|
+
if not song_info.with_valid_download_url:
|
|
150
|
+
params = {'format': 'aac|mp3', 'rid': search_result['MUSICRID'].removeprefix('MUSIC_'), 'type': 'convert_url3', 'response': 'url', 'br': '320kmp3'}
|
|
151
|
+
try:
|
|
152
|
+
resp = self.get('http://antiserver.kuwo.cn/anti.s', params=params, **request_overrides)
|
|
153
|
+
resp.raise_for_status()
|
|
154
|
+
download_result = resp2json(resp=resp)
|
|
155
|
+
download_url = download_result['url']
|
|
156
|
+
ext = download_url.split('.')[-1].split('?')[0] or 'mp3'
|
|
157
|
+
song_info = SongInfo(
|
|
158
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
159
|
+
ext=ext, duration_s=search_result.get('DURATION', 0), duration=seconds2hms(search_result.get('DURATION', 0)),
|
|
160
|
+
raw_data={'search': search_result, 'download': download_result},
|
|
161
|
+
)
|
|
162
|
+
except:
|
|
163
|
+
continue
|
|
164
|
+
if not song_info.with_valid_download_url: continue
|
|
165
|
+
# ----parse other information
|
|
166
|
+
song_info.update(dict(
|
|
167
|
+
song_name=legalizestring(search_result.get('SONGNAME', 'NULL'), replace_null_string='NULL'),
|
|
168
|
+
singers=legalizestring(search_result.get('ARTIST', 'NULL'), replace_null_string='NULL'),
|
|
169
|
+
album=legalizestring(search_result.get('ALBUM', 'NULL'), replace_null_string='NULL'),
|
|
170
|
+
identifier=search_result['MUSICRID'],
|
|
171
|
+
))
|
|
172
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
173
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
174
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
175
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
176
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
177
|
+
# --lyric results
|
|
178
|
+
params = {'musicId': search_result['MUSICRID'].removeprefix('MUSIC_'), 'httpsStatus': '1'}
|
|
179
|
+
try:
|
|
180
|
+
resp = self.get('http://m.kuwo.cn/newh5/singles/songinfoandlrc', params=params, **request_overrides)
|
|
181
|
+
resp.raise_for_status()
|
|
182
|
+
lyric_result: dict = resp2json(resp)
|
|
183
|
+
lyric = lyric_result.get('data', {}).get('lrclist', []) or 'NULL'
|
|
184
|
+
except:
|
|
185
|
+
lyric_result, lyric = {}, 'NULL'
|
|
186
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
187
|
+
song_info.lyric = lyric
|
|
188
|
+
# --append to song_infos
|
|
189
|
+
song_infos.append(song_info)
|
|
190
|
+
# --judgement for search_size
|
|
191
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
192
|
+
# --update progress
|
|
193
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
194
|
+
# failure
|
|
195
|
+
except Exception as err:
|
|
196
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
197
|
+
# return
|
|
198
|
+
return song_infos
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of LizhiMusicClient: https://www.lizhi.fm/
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import copy
|
|
10
|
+
from .base import BaseMusicClient
|
|
11
|
+
from urllib.parse import urlencode
|
|
12
|
+
from rich.progress import Progress
|
|
13
|
+
from ..utils import legalizestring, resp2json, seconds2hms, usesearchheaderscookies, SongInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
'''LizhiMusicClient'''
|
|
17
|
+
class LizhiMusicClient(BaseMusicClient):
|
|
18
|
+
source = 'LizhiMusicClient'
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super(LizhiMusicClient, self).__init__(**kwargs)
|
|
21
|
+
self.default_search_headers = {
|
|
22
|
+
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
|
|
23
|
+
'Referer': 'https://m.lizhi.fm',
|
|
24
|
+
}
|
|
25
|
+
self.default_download_headers = {
|
|
26
|
+
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
|
|
27
|
+
}
|
|
28
|
+
self.default_headers = self.default_search_headers
|
|
29
|
+
self._initsession()
|
|
30
|
+
'''_constructsearchurls'''
|
|
31
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
32
|
+
# init
|
|
33
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
34
|
+
# search rules
|
|
35
|
+
default_rule = {'deviceId': "h5-b6ef91a9-3dbb-c716-1fdd-43ba08851150", "keywords": keyword, "page": 1, "receiptData": ""}
|
|
36
|
+
default_rule.update(rule)
|
|
37
|
+
# construct search urls based on search rules
|
|
38
|
+
base_url = 'https://m.lizhi.fm/vodapi/search/voice?'
|
|
39
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
40
|
+
while self.search_size_per_source > count:
|
|
41
|
+
page_rule = copy.deepcopy(default_rule)
|
|
42
|
+
page_rule['page'] = int(count // page_size)
|
|
43
|
+
if len(search_urls) > 0:
|
|
44
|
+
try:
|
|
45
|
+
resp = self.get(search_urls[-1], **request_overrides)
|
|
46
|
+
receipt_data = resp2json(resp)['receiptData']
|
|
47
|
+
except:
|
|
48
|
+
receipt_data = ""
|
|
49
|
+
page_rule['receiptData'] = receipt_data
|
|
50
|
+
search_urls.append(base_url + urlencode(page_rule))
|
|
51
|
+
count += page_size
|
|
52
|
+
# return
|
|
53
|
+
return search_urls
|
|
54
|
+
'''_search'''
|
|
55
|
+
@usesearchheaderscookies
|
|
56
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
57
|
+
# init
|
|
58
|
+
request_overrides = request_overrides or {}
|
|
59
|
+
# successful
|
|
60
|
+
try:
|
|
61
|
+
# --search results
|
|
62
|
+
resp = self.get(search_url, **request_overrides)
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
search_results = resp2json(resp)['data']
|
|
65
|
+
for search_result in search_results:
|
|
66
|
+
# --download results
|
|
67
|
+
if (not isinstance(search_result, dict)) or ('userInfo' not in search_result) or ('voiceInfo' not in search_result) or ('voicePlayProperty' not in search_result) or ('voiceId' not in search_result['voiceInfo']):
|
|
68
|
+
continue
|
|
69
|
+
song_info = SongInfo(source=self.source)
|
|
70
|
+
download_url = search_result['voicePlayProperty'].get('trackUrl', '')
|
|
71
|
+
if not download_url: continue
|
|
72
|
+
for quality in ['_ud.mp3', '_hd.mp3', '_sd.m4a']:
|
|
73
|
+
download_url: str = download_url[:-7] + quality
|
|
74
|
+
ext = download_url.split('.')[-1].split('?')[0] or 'mp3'
|
|
75
|
+
song_info = SongInfo(
|
|
76
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides), ext=ext,
|
|
77
|
+
raw_data={'search': search_result, 'download': {}},
|
|
78
|
+
)
|
|
79
|
+
if song_info.with_valid_download_url: break
|
|
80
|
+
if not song_info.with_valid_download_url: continue
|
|
81
|
+
song_info.update(
|
|
82
|
+
duration=seconds2hms(search_result['voiceInfo'].get('duration', 0)), duration_s=search_result['voiceInfo'].get('duration', 0)
|
|
83
|
+
)
|
|
84
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
85
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
86
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
87
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
88
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
89
|
+
lyric_result, lyric = dict(), 'NULL'
|
|
90
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
91
|
+
song_info.update(dict(
|
|
92
|
+
lyric=lyric, song_name=legalizestring(search_result['voiceInfo'].get('name', 'NULL'), replace_null_string='NULL'),
|
|
93
|
+
singers=legalizestring(search_result['userInfo'].get('name', 'NULL'), replace_null_string='NULL'),
|
|
94
|
+
album=legalizestring(search_result['voiceInfo'].get('lableName', 'NULL'), replace_null_string='NULL'),
|
|
95
|
+
identifier=search_result['voiceInfo']['voiceId'],
|
|
96
|
+
))
|
|
97
|
+
# --append to song_infos
|
|
98
|
+
song_infos.append(song_info)
|
|
99
|
+
# --judgement for search_size
|
|
100
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
101
|
+
# --update progress
|
|
102
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
103
|
+
# failure
|
|
104
|
+
except Exception as err:
|
|
105
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
106
|
+
# return
|
|
107
|
+
return song_infos
|