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,118 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of BilibiliMusicClient: https://www.bilibili.com/audio/home/?type=9
|
|
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, usesearchheaderscookies, seconds2hms, safeextractfromdict, SongInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
'''BilibiliMusicClient'''
|
|
17
|
+
class BilibiliMusicClient(BaseMusicClient):
|
|
18
|
+
source = 'BilibiliMusicClient'
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super(BilibiliMusicClient, 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/121.0.0.0 Safari/537.36 Edg/121.0.0.0",
|
|
23
|
+
"Sec-Ch-Ua": '"Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"',
|
|
24
|
+
"Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Accept-Encoding": "gzip, deflate",
|
|
25
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5",
|
|
26
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
27
|
+
"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Referer": "https://www.bilibili.com/",
|
|
28
|
+
}
|
|
29
|
+
self.default_download_headers = copy.deepcopy(self.default_search_headers)
|
|
30
|
+
self.default_headers = self.default_search_headers
|
|
31
|
+
if not self.default_cookies:
|
|
32
|
+
self.default_search_cookies = {
|
|
33
|
+
"buvid3": "2E109C72-251F-3827-FA8E-921FA0D7EC5291319infoc", "b_nut": "1676213591", "i-wanna-go-back": "-1", "_uuid": "2B2D7A6C-8310C-1167-F548-2F1095A6E93F290252infoc",
|
|
34
|
+
"buvid4": "31696B5F-BB23-8F2B-3310-8B3C55FB49D491966-023021222-WcoPnBbwgLUAZ6TJuAUN8Q%3D%3D", "CURRENT_FNVAL": "4048", "DedeUserID": "520271156",
|
|
35
|
+
"DedeUserID__ckMd5": "66450f2302095cc5", "nostalgia_conf": "-1", "rpdid": "|(JY))RmR~|u0J'uY~YkuJ~Ru", "buvid_fp_plain": "undefined", "b_ut": "5",
|
|
36
|
+
"hit-dyn-v2": "1", "LIVE_BUVID": "AUTO8716766313471956", "hit-new-style-dyn": "1", "CURRENT_PID": "418c8490-cadb-11ed-b23b-dd640f2e1c14",
|
|
37
|
+
"FEED_LIVE_VERSION": "V8", "header_theme_version": "CLOSE", "CURRENT_QUALITY": "80", "enable_web_push": "DISABLE", "buvid_fp": "52ad4773acad74caefdb23875d5217cd",
|
|
38
|
+
"PVID": "1", "home_feed_column": "5", "SESSDATA": "8036f42c%2C1719895843%2C19675%2A12CjATThdxG8TyQ2panBpBQcmT0gDKjexwc-zXNGiMnIQ2I9oLVmOiE9YkLao2_aawEhoSVlhGY05PVjVkZWM0T042Z2hZRXBOdElYWXhJa3RpVmZ0M3NvcWw1N0tPcGRVSmRoOVNQZnNHT1JHS05yR1Y1MUFLX3RXeXVJa3NjbEVBQkUxRVN6RFRRIIEC",
|
|
39
|
+
"bili_jct": "4c583b61b86b16d812a7804078828688", "sid": "8dt1ioao", "bili_ticket": "eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDQ2MjUzNjAsImlhdCI6MTcwNDM2NjEwMCwicGx0IjotMX0.4E-V4K2y452cy6eexwY2x_q3-xgcNF2qtugddiuF8d4",
|
|
40
|
+
"bili_ticket_expires": "1704625300", "fingerprint": "847f1839b443252d91ff0df7465fa8d9", "browser_resolution": "1912-924", "bp_video_offset_520271156": "883089613008142344",
|
|
41
|
+
}
|
|
42
|
+
self.default_cookies = self.default_search_cookies
|
|
43
|
+
self.default_download_cookies = self.default_search_cookies
|
|
44
|
+
self._initsession()
|
|
45
|
+
'''_constructsearchurls'''
|
|
46
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
47
|
+
# init
|
|
48
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
49
|
+
# search rules
|
|
50
|
+
default_rule = {
|
|
51
|
+
'__refresh__': 'true', '_extra': '', 'page': 1, 'page_size': self.search_size_per_page, 'platform': 'pc', 'highlight': '1',
|
|
52
|
+
'context': '', 'single_column': '0', 'keyword': keyword, 'category_id': '', 'search_type': 'video', 'dynamic_offset': '0',
|
|
53
|
+
'preload': 'true', 'com2co': 'true'
|
|
54
|
+
}
|
|
55
|
+
default_rule.update(rule)
|
|
56
|
+
# construct search urls based on search rules
|
|
57
|
+
base_url = 'https://api.bilibili.com/x/web-interface/search/type?'
|
|
58
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
59
|
+
while self.search_size_per_source > count:
|
|
60
|
+
page_rule = copy.deepcopy(default_rule)
|
|
61
|
+
page_rule['page_size'] = page_size
|
|
62
|
+
page_rule['page'] = int(count // page_size) + 1
|
|
63
|
+
search_urls.append(base_url + urlencode(page_rule))
|
|
64
|
+
count += page_size
|
|
65
|
+
# return
|
|
66
|
+
return search_urls
|
|
67
|
+
'''_search'''
|
|
68
|
+
@usesearchheaderscookies
|
|
69
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
70
|
+
# init
|
|
71
|
+
request_overrides = request_overrides or {}
|
|
72
|
+
# successful
|
|
73
|
+
try:
|
|
74
|
+
# --search results
|
|
75
|
+
resp = self.get(search_url, **request_overrides)
|
|
76
|
+
resp.raise_for_status()
|
|
77
|
+
search_results = resp2json(resp)['data']['result']
|
|
78
|
+
for search_result in search_results:
|
|
79
|
+
# --download results
|
|
80
|
+
if not isinstance(search_result, dict) or ('id' not in search_result) or ('bvid' not in search_result):
|
|
81
|
+
continue
|
|
82
|
+
song_info = SongInfo(source=self.source)
|
|
83
|
+
try:
|
|
84
|
+
resp = self.get(f"https://api.bilibili.com/x/web-interface/view?bvid={search_result['bvid']}", **request_overrides)
|
|
85
|
+
resp.raise_for_status()
|
|
86
|
+
pages = resp2json(resp=resp)['data']['pages']
|
|
87
|
+
episodes = [(page["cid"], page["part"]) for page in pages] # we only pick the first one
|
|
88
|
+
cid, episode_name = episodes[0]
|
|
89
|
+
resp = self.get(f"https://api.bilibili.com/x/player/playurl?fnval=16&bvid={search_result['bvid']}&cid={cid}")
|
|
90
|
+
download_result = resp2json(resp=resp)
|
|
91
|
+
audios_sorted = sorted(download_result["data"]["dash"]["audio"], key=lambda x: x.get("bandwidth", 0), reverse=True)
|
|
92
|
+
except:
|
|
93
|
+
continue
|
|
94
|
+
download_url = audios_sorted[0].get('baseUrl') or audios_sorted[0].get('base_url') or audios_sorted[0].get('backupUrl') or audios_sorted[0].get('backup_url')
|
|
95
|
+
if not download_url: continue
|
|
96
|
+
if isinstance(download_url, list): download_url = download_url[0]
|
|
97
|
+
song_info = SongInfo(
|
|
98
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
99
|
+
ext='m4a', raw_data={'search': search_result, 'download': download_result, 'lyric': {}}, lyric='NULL', duration_s=download_result["data"]["dash"].get('duration', 0),
|
|
100
|
+
duration=seconds2hms(download_result["data"]["dash"].get('duration', 0)), file_size='NULL', song_name=legalizestring(episode_name, replace_null_string='NULL'),
|
|
101
|
+
singers=legalizestring(safeextractfromdict(search_result, ['author'], ""), replace_null_string='NULL'), album='NULL', identifier=f"{search_result['bvid']}_{cid}"
|
|
102
|
+
)
|
|
103
|
+
if not song_info.with_valid_download_url: continue
|
|
104
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(download_url, request_overrides)
|
|
105
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
106
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
107
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
108
|
+
# --append to song_infos
|
|
109
|
+
song_infos.append(song_info)
|
|
110
|
+
# --judgement for search_size
|
|
111
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
112
|
+
# --update progress
|
|
113
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
114
|
+
# failure
|
|
115
|
+
except Exception as err:
|
|
116
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
117
|
+
# return
|
|
118
|
+
return song_infos
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of BuguyyMusicClient: https://buguyy.top/
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import re
|
|
10
|
+
import copy
|
|
11
|
+
from .base import BaseMusicClient
|
|
12
|
+
from urllib.parse import urlencode
|
|
13
|
+
from rich.progress import Progress
|
|
14
|
+
from ..utils import legalizestring, usesearchheaderscookies, resp2json, safeextractfromdict, SongInfo, QuarkParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
'''BuguyyMusicClient'''
|
|
18
|
+
class BuguyyMusicClient(BaseMusicClient):
|
|
19
|
+
source = 'BuguyyMusicClient'
|
|
20
|
+
def __init__(self, **kwargs):
|
|
21
|
+
super(BuguyyMusicClient, self).__init__(**kwargs)
|
|
22
|
+
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.')
|
|
23
|
+
self.default_search_headers = {
|
|
24
|
+
"accept": "application/json, text/plain, */*",
|
|
25
|
+
"accept-encoding": "gzip, deflate, br, zstd",
|
|
26
|
+
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
27
|
+
"origin": "https://buguyy.top",
|
|
28
|
+
"priority": "u=1, i",
|
|
29
|
+
"referer": "https://buguyy.top/",
|
|
30
|
+
"sec-ch-ua": "\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\"",
|
|
31
|
+
"sec-ch-ua-mobile": "?0",
|
|
32
|
+
"sec-ch-ua-platform": "\"Windows\"",
|
|
33
|
+
"sec-fetch-dest": "empty",
|
|
34
|
+
"sec-fetch-mode": "cors",
|
|
35
|
+
"sec-fetch-site": "same-site",
|
|
36
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
|
37
|
+
}
|
|
38
|
+
self.default_download_headers = {
|
|
39
|
+
'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',
|
|
40
|
+
}
|
|
41
|
+
self.default_headers = self.default_search_headers
|
|
42
|
+
self._initsession()
|
|
43
|
+
'''_constructsearchurls'''
|
|
44
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
45
|
+
# init
|
|
46
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
47
|
+
# search rules
|
|
48
|
+
default_rule = {'keyword': keyword}
|
|
49
|
+
default_rule.update(rule)
|
|
50
|
+
# construct search urls based on search rules
|
|
51
|
+
base_url = 'https://a.buguyy.top/newapi/search.php?'
|
|
52
|
+
page_rule = copy.deepcopy(default_rule)
|
|
53
|
+
search_urls = [base_url + urlencode(page_rule)]
|
|
54
|
+
self.search_size_per_page = self.search_size_per_source
|
|
55
|
+
# return
|
|
56
|
+
return search_urls
|
|
57
|
+
'''_search'''
|
|
58
|
+
@usesearchheaderscookies
|
|
59
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
60
|
+
# init
|
|
61
|
+
request_overrides = request_overrides or {}
|
|
62
|
+
# successful
|
|
63
|
+
try:
|
|
64
|
+
# --search results
|
|
65
|
+
resp = self.get(search_url, verify=False, **request_overrides)
|
|
66
|
+
resp.raise_for_status()
|
|
67
|
+
search_results = resp2json(resp=resp)['data']['list']
|
|
68
|
+
for search_result in search_results:
|
|
69
|
+
# --download results
|
|
70
|
+
if not isinstance(search_result, dict) or ('id' not in search_result):
|
|
71
|
+
continue
|
|
72
|
+
song_info = SongInfo(source=self.source)
|
|
73
|
+
# ----parse from quark links
|
|
74
|
+
if self.quark_parser_config.get('cookies'):
|
|
75
|
+
quark_download_urls = [search_result.get('downurl', ''), search_result.get('ktmdownurl', '')]
|
|
76
|
+
for quark_download_url in quark_download_urls:
|
|
77
|
+
song_info = SongInfo(source=self.source)
|
|
78
|
+
try:
|
|
79
|
+
m = re.search(r"WAV#(https?://[^#]+)", quark_download_url)
|
|
80
|
+
quark_wav_download_url = m.group(1)
|
|
81
|
+
download_result, download_url = QuarkParser.parsefromurl(quark_wav_download_url, **self.quark_parser_config)
|
|
82
|
+
if not download_url: continue
|
|
83
|
+
download_url_status = self.quark_audio_link_tester.test(download_url, request_overrides)
|
|
84
|
+
download_url_status['probe_status'] = self.quark_audio_link_tester.probe(download_url, request_overrides)
|
|
85
|
+
ext = download_url_status['probe_status']['ext']
|
|
86
|
+
if ext == 'NULL': ext = 'wav'
|
|
87
|
+
song_info.update(dict(
|
|
88
|
+
download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
|
|
89
|
+
default_download_headers=self.quark_default_download_headers, ext=ext, file_size=download_url_status['probe_status']['file_size']
|
|
90
|
+
))
|
|
91
|
+
if song_info.with_valid_download_url: break
|
|
92
|
+
except:
|
|
93
|
+
continue
|
|
94
|
+
# ----parse from play url
|
|
95
|
+
lyric_result = {}
|
|
96
|
+
if not song_info.with_valid_download_url:
|
|
97
|
+
song_info = SongInfo(source=self.source)
|
|
98
|
+
try:
|
|
99
|
+
resp = self.get(f'https://a.buguyy.top/newapi/geturl2.php?id={search_result["id"]}', verify=False, **request_overrides)
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
download_result = resp2json(resp=resp)
|
|
102
|
+
download_url = safeextractfromdict(download_result, ['data', 'url'], '')
|
|
103
|
+
if not download_url: continue
|
|
104
|
+
download_url_status = self.audio_link_tester.test(download_url, request_overrides)
|
|
105
|
+
download_url_status['probe_status'] = self.audio_link_tester.probe(download_url, request_overrides)
|
|
106
|
+
ext = download_url_status['probe_status']['ext']
|
|
107
|
+
if ext == 'NULL': download_url.split('.')[-1].split('?')[0] or 'mp3'
|
|
108
|
+
song_info.update(dict(
|
|
109
|
+
download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
|
|
110
|
+
ext=ext, file_size=download_url_status['probe_status']['file_size']
|
|
111
|
+
))
|
|
112
|
+
except:
|
|
113
|
+
continue
|
|
114
|
+
lyric_result = copy.deepcopy(download_result)
|
|
115
|
+
else:
|
|
116
|
+
try:
|
|
117
|
+
resp = self.get(f'https://a.buguyy.top/newapi/geturl2.php?id={search_result["id"]}', verify=False, **request_overrides)
|
|
118
|
+
resp.raise_for_status()
|
|
119
|
+
lyric_result = resp2json(resp=resp)
|
|
120
|
+
except:
|
|
121
|
+
pass
|
|
122
|
+
if not song_info.with_valid_download_url: continue
|
|
123
|
+
# ----parse more infos
|
|
124
|
+
try:
|
|
125
|
+
duration = '{:02d}:{:02d}:{:02d}'.format(*([0,0,0] + list(map(int, re.findall(r'\d+', safeextractfromdict(lyric_result, ['data', 'duration'], '')))))[-3:])
|
|
126
|
+
if duration == '00:00:00': duration = '-:-:-'
|
|
127
|
+
except:
|
|
128
|
+
duration = '-:-:-'
|
|
129
|
+
lyric = safeextractfromdict(lyric_result, ['data', 'lrc'], '')
|
|
130
|
+
if not lyric or '歌词获取失败' in lyric: lyric = 'NULL'
|
|
131
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
132
|
+
song_info.update(dict(
|
|
133
|
+
lyric=lyric, duration=duration, song_name=legalizestring(search_result.get('title', 'NULL'), replace_null_string='NULL'),
|
|
134
|
+
singers=legalizestring(search_result.get('singer', 'NULL'), replace_null_string='NULL'),
|
|
135
|
+
album=legalizestring(safeextractfromdict(lyric_result, ['data', 'album'], ''), replace_null_string='NULL'),
|
|
136
|
+
identifier=search_result['id'],
|
|
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,153 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of FangpiMusicClient: https://www.fangpi.net/
|
|
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
|
+
'''FangpiMusicClient'''
|
|
19
|
+
class FangpiMusicClient(BaseMusicClient):
|
|
20
|
+
source = 'FangpiMusicClient'
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
super(FangpiMusicClient, 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.fangpi.net/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
|
+
search_results, base_url = [], "https://www.fangpi.net"
|
|
45
|
+
for row in soup.select('div.card.mb-1 div.card-text > div.row'):
|
|
46
|
+
link = row.select_one('a.music-link')
|
|
47
|
+
if not link: continue
|
|
48
|
+
href = link.get('href', '').strip()
|
|
49
|
+
if not href: continue
|
|
50
|
+
url = urljoin(base_url, href)
|
|
51
|
+
title_span = link.select_one('.music-title span')
|
|
52
|
+
if title_span: title = title_span.get_text(strip=True)
|
|
53
|
+
else: title = link.get_text(strip=True)
|
|
54
|
+
artist_tag = link.find('small')
|
|
55
|
+
artist = artist_tag.get_text(strip=True) if artist_tag else ""
|
|
56
|
+
search_results.append({"title": title, "artist": artist, "url": url})
|
|
57
|
+
return search_results
|
|
58
|
+
'''_search'''
|
|
59
|
+
@usesearchheaderscookies
|
|
60
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
61
|
+
# init
|
|
62
|
+
request_overrides = request_overrides or {}
|
|
63
|
+
# successful
|
|
64
|
+
try:
|
|
65
|
+
# --search results
|
|
66
|
+
resp = self.get(search_url, **request_overrides)
|
|
67
|
+
resp.raise_for_status()
|
|
68
|
+
search_results = self._parsesearchresultsfromhtml(resp.text)
|
|
69
|
+
for search_result in search_results:
|
|
70
|
+
# --download results
|
|
71
|
+
if not isinstance(search_result, dict) or ('url' not in search_result):
|
|
72
|
+
continue
|
|
73
|
+
song_info = SongInfo(source=self.source)
|
|
74
|
+
# ----fetch basic information
|
|
75
|
+
try:
|
|
76
|
+
resp = self.get(search_result['url'], **request_overrides)
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
soup = BeautifulSoup(resp.text, "lxml")
|
|
79
|
+
script_tag = soup.find("script", string=re.compile(r"window\.appData"))
|
|
80
|
+
if script_tag is None: continue
|
|
81
|
+
js_text: str = script_tag.string
|
|
82
|
+
m = re.search(r"window\.appData\s*=\s*(\{.*?\})\s*;", js_text, re.S)
|
|
83
|
+
if not m: continue
|
|
84
|
+
download_result = json_repair.loads(m.group(1))
|
|
85
|
+
except:
|
|
86
|
+
continue
|
|
87
|
+
# ----parse from quark links
|
|
88
|
+
if self.quark_parser_config.get('cookies'):
|
|
89
|
+
quark_download_urls = download_result.get('mp3_extra_urls', [])
|
|
90
|
+
for quark_download_url in quark_download_urls:
|
|
91
|
+
song_info = SongInfo(source=self.source)
|
|
92
|
+
try:
|
|
93
|
+
quark_wav_download_url = quark_download_url['share_link']
|
|
94
|
+
download_result['quark_parse_result'], download_url = QuarkParser.parsefromurl(quark_wav_download_url, **self.quark_parser_config)
|
|
95
|
+
if not download_url: continue
|
|
96
|
+
download_url_status = self.quark_audio_link_tester.test(download_url, request_overrides)
|
|
97
|
+
download_url_status['probe_status'] = self.quark_audio_link_tester.probe(download_url, request_overrides)
|
|
98
|
+
ext = download_url_status['probe_status']['ext']
|
|
99
|
+
if ext == 'NULL': ext = 'mp3'
|
|
100
|
+
song_info.update(dict(
|
|
101
|
+
download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
|
|
102
|
+
default_download_headers=self.quark_default_download_headers, ext=ext, file_size=download_url_status['probe_status']['file_size']
|
|
103
|
+
))
|
|
104
|
+
if song_info.with_valid_download_url: break
|
|
105
|
+
except:
|
|
106
|
+
continue
|
|
107
|
+
# ----parse from play url
|
|
108
|
+
if not song_info.with_valid_download_url:
|
|
109
|
+
if 'play_id' not in download_result or not download_result['play_id']: continue
|
|
110
|
+
song_info = SongInfo(source=self.source)
|
|
111
|
+
try:
|
|
112
|
+
resp = self.post('https://www.fangpi.net/api/play-url', json={'id': download_result['play_id']}, **request_overrides)
|
|
113
|
+
resp.raise_for_status()
|
|
114
|
+
download_result['api/play-url'] = resp2json(resp=resp)
|
|
115
|
+
download_url = safeextractfromdict(download_result['api/play-url'], ['data', 'url'], '')
|
|
116
|
+
if not download_url: continue
|
|
117
|
+
download_url_status = self.audio_link_tester.test(download_url, request_overrides)
|
|
118
|
+
download_url_status['probe_status'] = self.audio_link_tester.probe(download_url, request_overrides)
|
|
119
|
+
ext = download_url_status['probe_status']['ext']
|
|
120
|
+
if ext == 'NULL': download_url.split('.')[-1].split('?')[0] or 'mp3'
|
|
121
|
+
song_info.update(dict(
|
|
122
|
+
download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
|
|
123
|
+
ext=ext, file_size=download_url_status['probe_status']['file_size']
|
|
124
|
+
))
|
|
125
|
+
except:
|
|
126
|
+
continue
|
|
127
|
+
if not song_info.with_valid_download_url: continue
|
|
128
|
+
# ----parse more infos
|
|
129
|
+
try:
|
|
130
|
+
lrc_div = soup.find("div", id="content-lrc")
|
|
131
|
+
lyric, lyric_result = lrc_div.get_text("\n", strip=True), {'lrc_div': str(lrc_div)}
|
|
132
|
+
except:
|
|
133
|
+
lyric, lyric_result = 'NULL', {}
|
|
134
|
+
format_duration = lambda d: "{:02}:{:02}:{:02}".format(*([0] * (3 - len(d.split(":"))) + list(map(int, d.split(":")))))
|
|
135
|
+
duration = format_duration(download_result.get('mp3_duration', '00:00:00') or '00:00:00')
|
|
136
|
+
if duration == '00:00:00': duration = '-:-:-'
|
|
137
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
138
|
+
song_info.update(dict(
|
|
139
|
+
lyric=lyric, duration=duration, song_name=legalizestring(download_result.get('mp3_title', 'NULL'), replace_null_string='NULL'),
|
|
140
|
+
singers=legalizestring(download_result.get('mp3_author', 'NULL'), replace_null_string='NULL'), album='NULL',
|
|
141
|
+
identifier=download_result.get('play_id') or song_info.download_url,
|
|
142
|
+
))
|
|
143
|
+
# --append to song_infos
|
|
144
|
+
song_infos.append(song_info)
|
|
145
|
+
# --judgement for search_size
|
|
146
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
147
|
+
# --update progress
|
|
148
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
149
|
+
# failure
|
|
150
|
+
except Exception as err:
|
|
151
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
152
|
+
# return
|
|
153
|
+
return song_infos
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of FiveSingMusicClient: https://5sing.kugou.com/index.html
|
|
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, byte2mb, resp2json, usesearchheaderscookies, SongInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
'''FiveSingMusicClient'''
|
|
17
|
+
class FiveSingMusicClient(BaseMusicClient):
|
|
18
|
+
source = 'FiveSingMusicClient'
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super(FiveSingMusicClient, 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',
|
|
23
|
+
}
|
|
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
|
+
'''_constructsearchurls'''
|
|
30
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
31
|
+
# init
|
|
32
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
33
|
+
# search rules
|
|
34
|
+
default_rule = {'keyword': keyword, 'sort': 1, 'page': 1, 'filter': 0, 'type': 0}
|
|
35
|
+
default_rule.update(rule)
|
|
36
|
+
# construct search urls based on search rules
|
|
37
|
+
base_url = 'http://search.5sing.kugou.com/home/json?'
|
|
38
|
+
search_urls, page_size, count = [], self.search_size_per_page, 0
|
|
39
|
+
while self.search_size_per_source > count:
|
|
40
|
+
page_rule = copy.deepcopy(default_rule)
|
|
41
|
+
page_rule['page'] = int(count // page_size) + 1
|
|
42
|
+
search_urls.append(base_url + urlencode(page_rule))
|
|
43
|
+
count += page_size
|
|
44
|
+
# return
|
|
45
|
+
return search_urls
|
|
46
|
+
'''_search'''
|
|
47
|
+
@usesearchheaderscookies
|
|
48
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
49
|
+
# init
|
|
50
|
+
request_overrides = request_overrides or {}
|
|
51
|
+
# successful
|
|
52
|
+
try:
|
|
53
|
+
# --search results
|
|
54
|
+
resp = self.get(search_url, **request_overrides)
|
|
55
|
+
resp.raise_for_status()
|
|
56
|
+
search_results = resp2json(resp)['list']
|
|
57
|
+
for search_result in search_results:
|
|
58
|
+
# --download results
|
|
59
|
+
if not isinstance(search_result, dict) or ('songId' not in search_result) or ('typeEname' not in search_result):
|
|
60
|
+
continue
|
|
61
|
+
song_info = SongInfo(source=self.source)
|
|
62
|
+
params = {'songid': str(search_result['songId']), 'songtype': search_result['typeEname']}
|
|
63
|
+
try:
|
|
64
|
+
resp = self.get('http://mobileapi.5sing.kugou.com/song/getSongUrl', params=params, **request_overrides)
|
|
65
|
+
resp.raise_for_status()
|
|
66
|
+
download_result: dict = resp2json(resp)
|
|
67
|
+
if download_result['code'] not in [1000, '1000']: continue
|
|
68
|
+
except:
|
|
69
|
+
continue
|
|
70
|
+
data: dict = download_result.get('data', {})
|
|
71
|
+
for quality in ['sq', 'hq', 'lq']:
|
|
72
|
+
download_url = data.get(f'{quality}url', '').strip() or data.get(f'{quality}url_backup', '').strip()
|
|
73
|
+
if not download_url: continue
|
|
74
|
+
song_info = SongInfo(
|
|
75
|
+
source=self.source, download_url=download_url, download_url_status=self.audio_link_tester.test(download_url, request_overrides),
|
|
76
|
+
ext = data.get(f'{quality}ext', 'mp3').strip() or 'mp3', file_size_bytes=data.get(f'{quality}size', 0),
|
|
77
|
+
file_size = byte2mb(data.get(f'{quality}size', 0)), raw_data={'search': search_result, 'download': download_result},
|
|
78
|
+
)
|
|
79
|
+
if song_info.with_valid_download_url: break
|
|
80
|
+
if not song_info.with_valid_download_url: continue
|
|
81
|
+
# --lyric results
|
|
82
|
+
params = {'songid': str(search_result['songId']), 'songtype': search_result['typeEname'], 'songfields': '', 'userfields': ''}
|
|
83
|
+
try:
|
|
84
|
+
resp = self.get('http://mobileapi.5sing.kugou.com/song/newget', params=params, **request_overrides)
|
|
85
|
+
resp.raise_for_status()
|
|
86
|
+
lyric_result: dict = resp2json(resp)
|
|
87
|
+
lyric = str(lyric_result.get('data', {}).get('dynamicWords', 'NULL')).strip() or 'NULL'
|
|
88
|
+
except:
|
|
89
|
+
lyric_result, lyric = {}, 'NULL'
|
|
90
|
+
# --update song_info
|
|
91
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
92
|
+
song_info.update(dict(
|
|
93
|
+
song_name=legalizestring(search_result.get('songName', 'NULL'), replace_null_string='NULL'),
|
|
94
|
+
singers=legalizestring(search_result.get('singer', 'NULL'), replace_null_string='NULL'),
|
|
95
|
+
album=legalizestring(lyric_result.get('data', {}).get('albumName', 'NULL'), replace_null_string='NULL'),
|
|
96
|
+
identifier=search_result['songId'], lyric=lyric, duration='-:-:-',
|
|
97
|
+
))
|
|
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
|