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
@@ -0,0 +1,148 @@
1
+ '''
2
+ Function:
3
+ Implementation of GequbaoMusicClient: https://www.gequbao.com/
4
+ Author:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
+ Charles的皮卡丘
8
+ '''
9
+ import re
10
+ import json_repair
11
+ from bs4 import BeautifulSoup
12
+ from urllib.parse import urljoin
13
+ from .base import BaseMusicClient
14
+ from rich.progress import Progress
15
+ from ..utils import legalizestring, usesearchheaderscookies, resp2json, safeextractfromdict, SongInfo, QuarkParser
16
+
17
+
18
+ '''GequbaoMusicClient'''
19
+ class GequbaoMusicClient(BaseMusicClient):
20
+ source = 'GequbaoMusicClient'
21
+ def __init__(self, **kwargs):
22
+ super(GequbaoMusicClient, self).__init__(**kwargs)
23
+ if not self.quark_parser_config.get('cookies'): self.logger_handle.warning(f'{self.source}.__init__ >>> "quark_parser_config" is not configured, so song downloads are restricted and only mp3 files can be downloaded.')
24
+ self.default_search_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_download_headers = {
28
+ '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',
29
+ }
30
+ self.default_headers = self.default_search_headers
31
+ self._initsession()
32
+ '''_constructsearchurls'''
33
+ def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
34
+ # init
35
+ rule, request_overrides = rule or {}, request_overrides or {}
36
+ # construct search urls
37
+ search_urls = [f'https://www.gequbao.com/s/{keyword}']
38
+ self.search_size_per_page = self.search_size_per_source
39
+ # return
40
+ return search_urls
41
+ '''_parsesearchresultsfromhtml'''
42
+ def _parsesearchresultsfromhtml(self, html_text: str):
43
+ soup = BeautifulSoup(html_text, "lxml")
44
+ base_url, search_results = "https://www.gequbao.com", []
45
+ for a in soup.select("a.music-link"):
46
+ href = urljoin(base_url, a.get("href", "").strip())
47
+ title_tag = a.select_one(".music-title span")
48
+ title = title_tag.get_text(strip=True) if title_tag else ""
49
+ artist_tag = a.select_one("small")
50
+ artist = artist_tag.get_text(strip=True) if artist_tag else ""
51
+ search_results.append({'href': href, 'title': title, 'artist': artist})
52
+ return search_results
53
+ '''_search'''
54
+ @usesearchheaderscookies
55
+ def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
56
+ # init
57
+ request_overrides = request_overrides or {}
58
+ # successful
59
+ try:
60
+ # --search results
61
+ resp = self.get(search_url, **request_overrides)
62
+ resp.raise_for_status()
63
+ search_results = self._parsesearchresultsfromhtml(resp.text)
64
+ for search_result in search_results:
65
+ # --download results
66
+ if not isinstance(search_result, dict) or ('href' not in search_result):
67
+ continue
68
+ song_info = SongInfo(source=self.source)
69
+ # ----fetch basic information
70
+ try:
71
+ resp = self.get(search_result['href'], **request_overrides)
72
+ resp.raise_for_status()
73
+ soup = BeautifulSoup(resp.text, "lxml")
74
+ script_tag = soup.find("script", string=re.compile(r"window\.appData"))
75
+ if script_tag is None: continue
76
+ js_text: str = script_tag.string
77
+ m = re.search(r"window\.appData\s*=\s*(\{.*?\})\s*;", js_text, re.S)
78
+ if not m: continue
79
+ download_result = json_repair.loads(m.group(1))
80
+ except:
81
+ continue
82
+ # ----parse from quark links
83
+ if self.quark_parser_config.get('cookies'):
84
+ quark_download_urls = download_result.get('mp3_extra_urls', [])
85
+ for quark_download_url in quark_download_urls:
86
+ song_info = SongInfo(source=self.source)
87
+ try:
88
+ quark_wav_download_url = quark_download_url['share_link']
89
+ download_result['quark_parse_result'], download_url = QuarkParser.parsefromurl(quark_wav_download_url, **self.quark_parser_config)
90
+ if not download_url: continue
91
+ download_url_status = self.quark_audio_link_tester.test(download_url, request_overrides)
92
+ download_url_status['probe_status'] = self.quark_audio_link_tester.probe(download_url, request_overrides)
93
+ ext = download_url_status['probe_status']['ext']
94
+ if ext == 'NULL': ext = 'mp3'
95
+ song_info.update(dict(
96
+ download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
97
+ default_download_headers=self.quark_default_download_headers, ext=ext, file_size=download_url_status['probe_status']['file_size']
98
+ ))
99
+ if song_info.with_valid_download_url: break
100
+ except:
101
+ continue
102
+ # ----parse from play url
103
+ if not song_info.with_valid_download_url:
104
+ if 'play_id' not in download_result or not download_result['play_id']: continue
105
+ song_info = SongInfo(source=self.source)
106
+ try:
107
+ resp = self.post('https://www.gequbao.com/api/play-url', json={'id': download_result['play_id']}, **request_overrides)
108
+ resp.raise_for_status()
109
+ download_result['api/play-url'] = resp2json(resp=resp)
110
+ download_url = safeextractfromdict(download_result['api/play-url'], ['data', 'url'], '')
111
+ if not download_url: continue
112
+ download_url_status = self.audio_link_tester.test(download_url, request_overrides)
113
+ download_url_status['probe_status'] = self.audio_link_tester.probe(download_url, request_overrides)
114
+ ext = download_url_status['probe_status']['ext']
115
+ if ext == 'NULL': download_url.split('.')[-1].split('?')[0] or 'mp3'
116
+ song_info.update(dict(
117
+ download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
118
+ ext=ext, file_size=download_url_status['probe_status']['file_size']
119
+ ))
120
+ except:
121
+ continue
122
+ if not song_info.with_valid_download_url: continue
123
+ # ----parse more infos
124
+ try:
125
+ lrc_div = soup.find("div", id="content-lrc")
126
+ lyric, lyric_result = lrc_div.get_text("\n", strip=True), {'lrc_div': str(lrc_div)}
127
+ except:
128
+ lyric, lyric_result = 'NULL', {}
129
+ format_duration = lambda d: "{:02}:{:02}:{:02}".format(*([0] * (3 - len(d.split(":"))) + list(map(int, d.split(":")))))
130
+ duration = format_duration(download_result.get('mp3_duration', '00:00:00') or '00:00:00')
131
+ if duration == '00:00:00': duration = '-:-:-'
132
+ song_info.raw_data['lyric'] = lyric_result
133
+ song_info.update(dict(
134
+ lyric=lyric, duration=duration, song_name=legalizestring(download_result.get('mp3_title', 'NULL'), replace_null_string='NULL'),
135
+ singers=legalizestring(download_result.get('mp3_author', 'NULL'), replace_null_string='NULL'), album='NULL',
136
+ identifier=download_result.get('play_id') or song_info.download_url,
137
+ ))
138
+ # --append to song_infos
139
+ song_infos.append(song_info)
140
+ # --judgement for search_size
141
+ if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
142
+ # --update progress
143
+ progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
144
+ # failure
145
+ except Exception as err:
146
+ progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
147
+ # return
148
+ return song_infos
@@ -0,0 +1,108 @@
1
+ '''
2
+ Function:
3
+ Implementation of JamendoMusicClient: https://www.jamendo.com/
4
+ Author:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
+ Charles的皮卡丘
8
+ '''
9
+ import copy
10
+ import random
11
+ import hashlib
12
+ from .base import BaseMusicClient
13
+ from urllib.parse import urlencode
14
+ from rich.progress import Progress
15
+ from ..utils import legalizestring, resp2json, usesearchheaderscookies, seconds2hms, safeextractfromdict, SongInfo
16
+
17
+
18
+ '''JamendoMusicClient'''
19
+ class JamendoMusicClient(BaseMusicClient):
20
+ source = 'JamendoMusicClient'
21
+ def __init__(self, **kwargs):
22
+ super(JamendoMusicClient, self).__init__(**kwargs)
23
+ self.default_search_headers = {
24
+ "referer": "https://www.jamendo.com/search?q=musicdl",
25
+ "sec-ch-ua": "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
26
+ "sec-ch-ua-mobile": "?0",
27
+ "sec-ch-ua-platform": "\"Windows\"",
28
+ "sec-fetch-dest": "empty",
29
+ "sec-fetch-mode": "cors",
30
+ "sec-fetch-site": "same-origin",
31
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
32
+ "x-jam-call": "$536ab7feabd2404af7b6e54b4db74039734b58b3*0.5310391483096057~",
33
+ "x-jam-version": "4gvfvv",
34
+ "x-requested-with": "XMLHttpRequest",
35
+ }
36
+ self.default_download_headers = {
37
+ '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',
38
+ }
39
+ self.default_headers = self.default_search_headers
40
+ self._initsession()
41
+ '''_makexjamcall'''
42
+ def _makexjamcall(self, path: str = '/api/search') -> str:
43
+ rand = str(random.random())
44
+ digest = hashlib.sha1((path + rand).encode("utf-8")).hexdigest()
45
+ return f"${digest}*{rand}~"
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 = {'query': keyword, 'type': 'track', 'limit': self.search_size_per_source, 'identities': 'www'}
52
+ default_rule.update(rule)
53
+ # construct search urls based on search rules
54
+ base_url = 'https://www.jamendo.com/api/search?'
55
+ page_rule = copy.deepcopy(default_rule)
56
+ search_urls = [base_url + urlencode(page_rule)]
57
+ self.search_size_per_page = self.search_size_per_source
58
+ # return
59
+ return search_urls
60
+ '''_search'''
61
+ @usesearchheaderscookies
62
+ def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
63
+ # init
64
+ request_overrides = request_overrides or {}
65
+ # successful
66
+ try:
67
+ # --search results
68
+ headers = copy.deepcopy(self.default_headers)
69
+ headers['x-jam-call'] = self._makexjamcall()
70
+ resp = self.get(search_url, headers=headers, **request_overrides)
71
+ resp.raise_for_status()
72
+ search_results = resp2json(resp)
73
+ for search_result in search_results:
74
+ # --download results
75
+ if not isinstance(search_result, dict) or ('id' not in search_result) or ('stream' not in search_result and 'download' not in search_result):
76
+ continue
77
+ song_info = SongInfo(source=self.source)
78
+ streams: dict = search_result.get('download') or search_result.get('stream')
79
+ for quality in ['flac', 'ogg', 'mp3']:
80
+ download_url = streams.get(quality, "")
81
+ if not download_url: continue
82
+ song_info = SongInfo(
83
+ source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
84
+ ext='mp3' if streams.get('mp3') else 'ogg', raw_data={'search': search_result, 'download': {}, 'lyric': {}}, lyric='NULL',
85
+ duration_s=search_result.get('duration', 0), duration=seconds2hms(search_result.get('duration', 0)),
86
+ song_name=legalizestring(safeextractfromdict(search_result, ['name'], ""), replace_null_string='NULL'),
87
+ singers=legalizestring(safeextractfromdict(search_result, ['artist', 'name'], ""), replace_null_string='NULL'),
88
+ album=legalizestring(safeextractfromdict(search_result, ['album', 'name'], ""), replace_null_string='NULL'),
89
+ identifier=search_result['id'],
90
+ )
91
+ if song_info.with_valid_download_url: break
92
+ if not song_info.with_valid_download_url: continue
93
+ song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(download_url, request_overrides)
94
+ ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
95
+ if file_size and file_size != 'NULL': song_info.file_size = file_size
96
+ if not song_info.file_size: song_info.file_size = 'NULL'
97
+ if ext and ext != 'NULL': song_info.ext = ext
98
+ # --append to song_infos
99
+ song_infos.append(song_info)
100
+ # --judgement for search_size
101
+ if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
102
+ # --update progress
103
+ progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
104
+ # failure
105
+ except Exception as err:
106
+ progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
107
+ # return
108
+ return song_infos
@@ -1,79 +1,115 @@
1
1
  '''
2
2
  Function:
3
- JOOX音乐下载: https://www.joox.com/cn/login
3
+ Implementation of JooxMusicClient: https://www.joox.com/intl
4
4
  Author:
5
- Charles
6
- 微信公众号:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
7
  Charles的皮卡丘
8
8
  '''
9
- import json
10
- import time
9
+ import copy
11
10
  import base64
12
- import requests
13
- from .base import Base
14
- from ..utils.misc import *
11
+ import json_repair
12
+ from .base import BaseMusicClient
13
+ from rich.progress import Progress
14
+ from urllib.parse import urlencode, urlparse, parse_qs
15
+ from ..utils import legalizestring, byte2mb, resp2json, seconds2hms, usesearchheaderscookies, SongInfo
15
16
 
16
17
 
17
- '''JOOX音乐下载类'''
18
- class joox(Base):
19
- def __init__(self, config, logger_handle, **kwargs):
20
- super(joox, self).__init__(config, logger_handle, **kwargs)
21
- self.source = 'joox'
22
- self.__initialize()
23
- '''歌曲搜索'''
24
- def search(self, keyword):
25
- self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword))
26
- cfg = self.config.copy()
27
- params = {
28
- 'country': 'hk',
29
- 'lang': 'zh_TW',
30
- 'search_input': keyword,
31
- 'sin': '0',
32
- 'ein': cfg['search_size_per_source']
33
- }
34
- response = self.session.get(self.search_url, headers=self.headers, params=params)
35
- all_items = response.json()['itemlist']
36
- songinfos = []
37
- for item in all_items:
38
- params = {
39
- 'songid': item['songid'],
40
- 'lang': 'zh_cn',
41
- 'country': 'hk',
42
- 'from_type': '-1',
43
- 'channel_id': '-1',
44
- '_': str(int(time.time()*1000))
45
- }
46
- response = self.session.get(self.songinfo_url, headers=self.headers, params=params)
47
- response_json = json.loads(response.text.replace('MusicInfoCallback(', '')[:-1])
48
- if response_json.get('code') != 0: continue
49
- for q_key in [('r320Url', '320'), ('r192Url', '192'), ('mp3Url', '128')]:
50
- download_url = response_json.get(q_key[0], '')
51
- if not download_url: continue
52
- filesize = str(round(int(json.loads(response_json['kbps_map'])[q_key[1]])/1024/1024, 2)) + 'MB'
53
- ext = 'mp3' if q_key[0] in ['r320Url', 'mp3Url'] else 'm4a'
54
- if not download_url: continue
55
- duration = int(item.get('playtime', 0))
56
- songinfo = {
57
- 'source': self.source,
58
- 'songid': str(item['songid']),
59
- 'singers': filterBadCharacter(','.join([base64.b64decode(s['name']).decode('utf-8') for s in item.get('singer_list', [])])),
60
- 'album': filterBadCharacter(response_json.get('malbum', '-')),
61
- 'songname': filterBadCharacter(response_json.get('msong', '-')),
62
- 'savedir': cfg['savedir'],
63
- 'savename': '_'.join([self.source, filterBadCharacter(response_json.get('msong', '-'))]),
64
- 'download_url': download_url,
65
- 'filesize': filesize,
66
- 'ext': ext,
67
- 'duration': seconds2hms(duration)
68
- }
69
- songinfos.append(songinfo)
70
- return songinfos
71
- '''初始化'''
72
- def __initialize(self):
73
- self.headers = {
74
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/605.1.15 (KHTML, like Gecko)',
18
+ '''JooxMusicClient'''
19
+ class JooxMusicClient(BaseMusicClient):
20
+ source = 'JooxMusicClient'
21
+ def __init__(self, **kwargs):
22
+ super(JooxMusicClient, 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/139.0.0.0 Safari/537.36',
75
25
  'Cookie': 'wmid=142420656; user_type=1; country=id; session_key=2a5d97d05dc8fe238150184eaf3519ad;',
76
26
  'X-Forwarded-For': '36.73.34.109'
77
27
  }
78
- self.search_url = 'https://api-jooxtt.sanook.com/web-fcgi-bin/web_search'
79
- self.songinfo_url = 'https://api.joox.com/web-fcgi-bin/web_get_songinfo'
28
+ self.default_download_headers = {
29
+ '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
+ }
31
+ self.default_headers = self.default_search_headers
32
+ self._initsession()
33
+ '''_constructsearchurls'''
34
+ def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
35
+ # init
36
+ rule, request_overrides = rule or {}, request_overrides or {}
37
+ # search rules
38
+ default_rule = {'country': 'sg', 'lang': 'zh_cn', 'keyword': keyword}
39
+ default_rule.update(rule)
40
+ # construct search urls based on search rules
41
+ base_url = 'https://cache.api.joox.com/openjoox/v3/search?'
42
+ page_rule = copy.deepcopy(default_rule)
43
+ search_urls = [base_url + urlencode(page_rule)]
44
+ self.search_size_per_page = self.search_size_per_source
45
+ # return
46
+ return search_urls
47
+ '''_search'''
48
+ @usesearchheaderscookies
49
+ def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
50
+ # init
51
+ request_overrides = request_overrides or {}
52
+ # successful
53
+ try:
54
+ # --search results
55
+ resp = self.session.get(search_url, **request_overrides)
56
+ resp.raise_for_status()
57
+ search_results = []
58
+ for section in resp2json(resp)['section_list']:
59
+ for items in section['item_list']:
60
+ search_results.extend(items.get('song', []))
61
+ parsed_search_url = parse_qs(urlparse(search_url).query, keep_blank_values=True)
62
+ lang, country = parsed_search_url['lang'][0], parsed_search_url['country'][0]
63
+ for search_result in search_results:
64
+ # --download results
65
+ if not isinstance(search_result, dict) or ('song_info' not in search_result) or ('id' not in search_result['song_info']):
66
+ continue
67
+ song_info = SongInfo(source=self.source)
68
+ params = {'songid': search_result['song_info']['id'], 'lang': lang, 'country': country}
69
+ try:
70
+ resp = self.get('https://api.joox.com/web-fcgi-bin/web_get_songinfo', params=params, **request_overrides)
71
+ resp.raise_for_status()
72
+ download_result = json_repair.loads(resp.text.replace('MusicInfoCallback(', '')[:-1])
73
+ kbps_map = json_repair.loads(download_result['kbps_map'])
74
+ except:
75
+ continue
76
+ for quality in [('r320Url', '320'), ('r192Url', '192'), ('mp3Url', '128'), ('m4aUrl', '96')]:
77
+ if (not kbps_map.get(quality[1])) or (not download_result.get(quality[0])):
78
+ continue
79
+ download_url: str = download_result.get(quality[0])
80
+ ext = download_url.split('.')[-1].split('?')[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), ext=ext,
83
+ raw_data={'search': search_result, 'download': download_result}, file_size_bytes=kbps_map.get(quality[1], 0),
84
+ file_size=byte2mb(kbps_map.get(quality[1], 0)), duration_s=download_result.get('minterval', 0), duration=seconds2hms(download_result.get('minterval', 0)),
85
+ )
86
+ if song_info.with_valid_download_url: break
87
+ if not song_info.with_valid_download_url: continue
88
+ song_info.update(dict(
89
+ song_name=legalizestring(search_result['song_info'].get('name', 'NULL'), replace_null_string='NULL'),
90
+ singers=legalizestring(', '.join([singer.get('name', 'NULL') for singer in search_result['song_info'].get('artist_list', [])]), replace_null_string='NULL'),
91
+ album=legalizestring(search_result['song_info'].get('album_name', 'NULL'), replace_null_string='NULL'),
92
+ identifier=search_result['song_info']['id'],
93
+ ))
94
+ # --lyric results
95
+ params = {'musicid': search_result['song_info']['id'], 'country': country, 'lang': lang}
96
+ try:
97
+ resp = self.get('https://api.joox.com/web-fcgi-bin/web_lyric', params=params, **request_overrides)
98
+ resp.raise_for_status()
99
+ lyric_result: dict = json_repair.loads(resp.text.replace('MusicJsonCallback(', '')[:-1]) or {}
100
+ lyric = base64.b64decode(lyric_result.get('lyric', '')).decode('utf-8') or 'NULL'
101
+ except:
102
+ lyric_result, lyric = dict(), 'NULL'
103
+ song_info.raw_data['lyric'] = lyric_result
104
+ song_info.lyric = lyric
105
+ # --append to song_infos
106
+ song_infos.append(song_info)
107
+ # --judgement for search_size
108
+ if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
109
+ # --update progress
110
+ progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
111
+ # failure
112
+ except Exception as err:
113
+ progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
114
+ # return
115
+ return song_infos