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
|
@@ -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
|
musicdl/modules/sources/joox.py
CHANGED
|
@@ -1,79 +1,115 @@
|
|
|
1
1
|
'''
|
|
2
2
|
Function:
|
|
3
|
-
|
|
3
|
+
Implementation of JooxMusicClient: https://www.joox.com/intl
|
|
4
4
|
Author:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
7
|
Charles的皮卡丘
|
|
8
8
|
'''
|
|
9
|
-
import
|
|
10
|
-
import time
|
|
9
|
+
import copy
|
|
11
10
|
import base64
|
|
12
|
-
import
|
|
13
|
-
from .base import
|
|
14
|
-
from
|
|
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
|
-
'''
|
|
18
|
-
class
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
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.
|
|
79
|
-
|
|
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
|