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,85 +1,138 @@
1
1
  '''
2
2
  Function:
3
- 酷狗音乐下载: http://www.kugou.com/
3
+ Implementation of KugouMusicClient: http://www.kugou.com/
4
4
  Author:
5
- Charles
6
- 微信公众号:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
7
  Charles的皮卡丘
8
8
  '''
9
- import time
10
- import requests
11
- from .base import Base
12
- from ..utils.misc import *
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 kugou(Base):
17
- def __init__(self, config, logger_handle, **kwargs):
18
- super(kugou, self).__init__(config, logger_handle, **kwargs)
19
- self.source = 'kugou'
20
- self.__initialize()
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
- response = self.session.get(self.search_url, headers=self.search_headers, params=params)
39
- all_items = response.json()['data']['lists']
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.hash_headers = {
81
- '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',
82
- 'Referer':'https://www.kugou.com/song/'
83
- }
84
- self.search_url = 'http://songsearch.kugou.com/song_search_v2'
85
- self.hash_url = 'https://wwwapi.kugou.com/yy/index.php'
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
@@ -1,78 +1,198 @@
1
1
  '''
2
2
  Function:
3
- 酷我音乐下载: http://www.kuwo.cn/
3
+ Implementation of KuwoMusicClient: http://www.kuwo.cn/
4
4
  Author:
5
- Charles
6
- 微信公众号:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
7
  Charles的皮卡丘
8
8
  '''
9
- import time
10
- import requests
11
- from .base import Base
12
- from ..utils.misc import *
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 kuwo(Base):
17
- def __init__(self, config, logger_handle, **kwargs):
18
- super(kuwo, self).__init__(config, logger_handle, **kwargs)
19
- self.source = 'kuwo'
20
- self.__initialize()
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
- response = self.session.get(self.search_url, headers=self.headers, params=params)
32
- all_items = response.json()['data']['list']
33
- songinfos = []
34
- for item in all_items:
35
- params = {
36
- 'format': 'mp3',
37
- 'rid': str(item['rid']),
38
- 'response': 'url',
39
- 'type': 'convert_url3',
40
- 'br': '128kmp3',
41
- 'from': 'web',
42
- 't': str(int(time.time()*1000)),
43
- 'reqId': 'de97aac1-73c3-11ea-a715-7de8a8cc7b68'
44
- }
45
- response = self.session.get(self.player_url, headers=self.headers, params=params)
46
- response_json = response.json()
47
- if response_json.get('code') != 200: continue
48
- download_url = response_json['url']
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
- filesize = '-MB'
51
- ext = download_url.split('.')[-1]
52
- duration = int(item.get('duration', 0))
53
- songinfo = {
54
- 'source': self.source,
55
- 'songid': str(item['rid']),
56
- 'singers': filterBadCharacter(item.get('artist', '-')),
57
- 'album': filterBadCharacter(item.get('album', '-')),
58
- 'songname': filterBadCharacter(item.get('name', '-')),
59
- 'savedir': cfg['savedir'],
60
- 'savename': '_'.join([self.source, filterBadCharacter(item.get('name', '-'))]),
61
- 'download_url': download_url,
62
- 'filesize': filesize,
63
- 'ext': ext,
64
- 'duration': seconds2hms(duration)
65
- }
66
- songinfos.append(songinfo)
67
- return songinfos
68
- '''初始化'''
69
- def __initialize(self):
70
- self.headers = {
71
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
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
- self.search_url = 'http://www.kuwo.cn/api/www/search/searchMusicBykeyWord'
78
- self.player_url = 'http://www.kuwo.cn/url'
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