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,144 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of YinyuedaoMusicClient: https://1mp3.top/
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import json_repair
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
from .base import BaseMusicClient
|
|
12
|
+
from rich.progress import Progress
|
|
13
|
+
from ..utils import legalizestring, usesearchheaderscookies, safeextractfromdict, SongInfo, QuarkParser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
'''YinyuedaoMusicClient'''
|
|
17
|
+
class YinyuedaoMusicClient(BaseMusicClient):
|
|
18
|
+
source = 'YinyuedaoMusicClient'
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super(YinyuedaoMusicClient, self).__init__(**kwargs)
|
|
21
|
+
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.')
|
|
22
|
+
self.default_search_headers = {
|
|
23
|
+
"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",
|
|
24
|
+
"accept-encoding": "gzip, deflate, br, zstd",
|
|
25
|
+
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
26
|
+
"priority": "u=0, i",
|
|
27
|
+
"referer": "https://1mp3.top/",
|
|
28
|
+
"sec-ch-ua": "\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\"",
|
|
29
|
+
"sec-ch-ua-mobile": "?0",
|
|
30
|
+
"sec-ch-ua-platform": "\"Windows\"",
|
|
31
|
+
"sec-fetch-dest": "document",
|
|
32
|
+
"sec-fetch-mode": "navigate",
|
|
33
|
+
"sec-fetch-site": "same-origin",
|
|
34
|
+
"sec-fetch-user": "?1",
|
|
35
|
+
"upgrade-insecure-requests": "1",
|
|
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
|
+
# construct search urls
|
|
48
|
+
search_urls = [f'https://1mp3.top/?key={keyword}&search=1']
|
|
49
|
+
self.search_size_per_page = self.search_size_per_source
|
|
50
|
+
# return
|
|
51
|
+
return search_urls
|
|
52
|
+
'''_parsesearchresultsfromhtml'''
|
|
53
|
+
def _parsesearchresultsfromhtml(self, html_text: str):
|
|
54
|
+
soup = BeautifulSoup(html_text, "lxml")
|
|
55
|
+
search_results = []
|
|
56
|
+
for li in soup.select("#musicList > li"):
|
|
57
|
+
music_id_attr = li.get("data-music-id")
|
|
58
|
+
music_title_attr = li.get("data-music-title")
|
|
59
|
+
music_singer_attr = li.get("data-music-singer")
|
|
60
|
+
music_cover_attr = li.get("data-music-cover")
|
|
61
|
+
a_download = li.select_one("a.download-btn")
|
|
62
|
+
music_json_str = a_download.get("data-music")
|
|
63
|
+
music_data = json_repair.loads(music_json_str)
|
|
64
|
+
search_results.append({
|
|
65
|
+
"id_attr": music_id_attr, "title_attr": music_title_attr, "singer_attr": music_singer_attr, "cover_attr": music_cover_attr,
|
|
66
|
+
"id": music_data.get("id"), "title": music_data.get("title"), "singer": music_data.get("singer"), "picurl": music_data.get("picurl"),
|
|
67
|
+
"create_time": music_data.get("create_time"), "mtype": music_data.get("mtype"), "downlist": music_data.get("downlist", []),
|
|
68
|
+
"ktmdownlist": music_data.get("ktmdownlist", []),
|
|
69
|
+
})
|
|
70
|
+
return search_results
|
|
71
|
+
'''_search'''
|
|
72
|
+
@usesearchheaderscookies
|
|
73
|
+
def _search(self, keyword: str = '', search_url: str = '', request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
74
|
+
# init
|
|
75
|
+
request_overrides = request_overrides or {}
|
|
76
|
+
# successful
|
|
77
|
+
try:
|
|
78
|
+
# --search results
|
|
79
|
+
resp = self.get(search_url, **request_overrides)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
search_results = self._parsesearchresultsfromhtml(resp.text)
|
|
82
|
+
for search_result in search_results:
|
|
83
|
+
# --download results
|
|
84
|
+
if not isinstance(search_result, dict) or ('id' not in search_result):
|
|
85
|
+
continue
|
|
86
|
+
song_info = SongInfo(source=self.source)
|
|
87
|
+
# ----parse from quark links
|
|
88
|
+
if self.quark_parser_config.get('cookies'):
|
|
89
|
+
quark_download_urls = [*search_result.get('downlist', []), *search_result.get('ktmdownlist', [])]
|
|
90
|
+
for quark_download_url in quark_download_urls:
|
|
91
|
+
song_fmt = safeextractfromdict(quark_download_url, ['format'], '')
|
|
92
|
+
if not song_fmt or song_fmt.lower() in ['mp3']: continue
|
|
93
|
+
song_info = SongInfo(source=self.source)
|
|
94
|
+
try:
|
|
95
|
+
quark_wav_download_url = quark_download_url['url']
|
|
96
|
+
download_result, download_url = QuarkParser.parsefromurl(quark_wav_download_url, **self.quark_parser_config)
|
|
97
|
+
if not download_url: continue
|
|
98
|
+
download_url_status = self.quark_audio_link_tester.test(download_url, request_overrides)
|
|
99
|
+
download_url_status['probe_status'] = self.quark_audio_link_tester.probe(download_url, request_overrides)
|
|
100
|
+
ext = download_url_status['probe_status']['ext']
|
|
101
|
+
if ext == 'NULL': ext = 'flac'
|
|
102
|
+
song_info.update(dict(
|
|
103
|
+
download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': download_result},
|
|
104
|
+
default_download_headers=self.quark_default_download_headers, ext=ext, file_size=download_url_status['probe_status']['file_size']
|
|
105
|
+
))
|
|
106
|
+
if song_info.with_valid_download_url: break
|
|
107
|
+
except:
|
|
108
|
+
continue
|
|
109
|
+
# ----parse from play url
|
|
110
|
+
if not song_info.with_valid_download_url:
|
|
111
|
+
song_info = SongInfo(source=self.source)
|
|
112
|
+
try:
|
|
113
|
+
resp = self.get(f'https://1mp3.top/include/geturl.php?id={search_result["id"]}', **request_overrides)
|
|
114
|
+
resp.raise_for_status()
|
|
115
|
+
download_url = resp.text.strip()
|
|
116
|
+
download_url_status = self.audio_link_tester.test(download_url, request_overrides)
|
|
117
|
+
download_url_status['probe_status'] = self.audio_link_tester.probe(download_url, request_overrides)
|
|
118
|
+
ext = download_url_status['probe_status']['ext']
|
|
119
|
+
if ext == 'NULL': download_url.split('.')[-1].split('?')[0] or 'mp3'
|
|
120
|
+
song_info.update(dict(
|
|
121
|
+
download_url=download_url, download_url_status=download_url_status, raw_data={'search': search_result, 'download': {}},
|
|
122
|
+
ext=ext, file_size=download_url_status['probe_status']['file_size']
|
|
123
|
+
))
|
|
124
|
+
except:
|
|
125
|
+
continue
|
|
126
|
+
if not song_info.with_valid_download_url: continue
|
|
127
|
+
# ----parse more infos
|
|
128
|
+
lyric_result, lyric = dict(), 'NULL'
|
|
129
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
130
|
+
song_info.update(dict(
|
|
131
|
+
lyric=lyric, duration='-:-:-', song_name=legalizestring(search_result.get('title', 'NULL'), replace_null_string='NULL'),
|
|
132
|
+
singers=legalizestring(search_result.get('singer', 'NULL'), replace_null_string='NULL'), album='NULL', identifier=search_result['id'],
|
|
133
|
+
))
|
|
134
|
+
# --append to song_infos
|
|
135
|
+
song_infos.append(song_info)
|
|
136
|
+
# --judgement for search_size
|
|
137
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
138
|
+
# --update progress
|
|
139
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
140
|
+
# failure
|
|
141
|
+
except Exception as err:
|
|
142
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
143
|
+
# return
|
|
144
|
+
return song_infos
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of YouTubeMusicClient: https://music.youtube.com/
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import copy
|
|
10
|
+
import random
|
|
11
|
+
from ytmusicapi import YTMusic
|
|
12
|
+
from .base import BaseMusicClient
|
|
13
|
+
from rich.progress import Progress
|
|
14
|
+
from ..utils.youtubeutils import YouTube
|
|
15
|
+
from ..utils import legalizestring, resp2json, usesearchheaderscookies, byte2mb, seconds2hms, usedownloadheaderscookies, touchdir, SongInfo, SongInfoUtils
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
'''constants'''
|
|
19
|
+
REPAIDAPI_KEYS = [
|
|
20
|
+
"1556f6ccb2msh64b807485156b33p12a66djsn7e197bc23f19", "323be663ccmsh35173cb3c5403c2p10f08fjsnad5cf7b49fa9", "7ca8a11abdmsh7eb89d767db710cp191ce7jsnf0274562b5b9", "14a93bf7a5msh51f11db7d121aeap1c4625jsn5cfbb89ff05b",
|
|
21
|
+
"6111b62d2dmshd13d20b55abbbe8p1f8551jsn2e425d987b94", "fcdf2c5ba1msh397175bf89fe87ep19fe51jsn76bc6eddb936", "ab47a3c2camsh4097dc8ff0fb89fp1d95e6jsna047ccc76ab6", "3b6e8de0bfmshf1110629d4a95b0p11b000jsn264f29acc2b0",
|
|
22
|
+
"9203689df7msh9f35abfe7467ce8p12ca71jsn625743a17f66", "ef908b5eeamsh50984dd5b890d6ep1ab791jsn2fb248ebf19a",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
'''YouTubeMusicClient'''
|
|
27
|
+
class YouTubeMusicClient(BaseMusicClient):
|
|
28
|
+
source = 'YouTubeMusicClient'
|
|
29
|
+
def __init__(self, **kwargs):
|
|
30
|
+
super(YouTubeMusicClient, self).__init__(**kwargs)
|
|
31
|
+
self.default_search_headers = {}
|
|
32
|
+
self.default_download_headers = {}
|
|
33
|
+
self.default_headers = self.default_search_headers
|
|
34
|
+
self._initsession()
|
|
35
|
+
'''_download'''
|
|
36
|
+
@usedownloadheaderscookies
|
|
37
|
+
def _download(self, song_info: SongInfo, request_overrides: dict = None, downloaded_song_infos: list = [], progress: Progress = None, song_progress_id: int = 0):
|
|
38
|
+
if isinstance(song_info.download_url, str): return super()._download(song_info=song_info, request_overrides=request_overrides, downloaded_song_infos=downloaded_song_infos, progress=progress, song_progress_id=song_progress_id)
|
|
39
|
+
request_overrides = request_overrides or {}
|
|
40
|
+
try:
|
|
41
|
+
touchdir(song_info.work_dir)
|
|
42
|
+
total_size, chunk_size, downloaded_size = int(song_info.download_url.filesize), song_info.get('chunk_size', 1024 * 1024), 0
|
|
43
|
+
progress.update(song_progress_id, total=total_size)
|
|
44
|
+
with open(song_info.save_path, "wb") as fp:
|
|
45
|
+
for chunk in song_info.download_url.iterchunks(chunk_size=chunk_size):
|
|
46
|
+
if not chunk: continue
|
|
47
|
+
fp.write(chunk)
|
|
48
|
+
downloaded_size = downloaded_size + len(chunk)
|
|
49
|
+
if total_size > 0:
|
|
50
|
+
downloading_text = "%0.2fMB/%0.2fMB" % (downloaded_size / 1024 / 1024, total_size / 1024 / 1024)
|
|
51
|
+
else:
|
|
52
|
+
progress.update(song_progress_id, total=downloaded_size)
|
|
53
|
+
downloading_text = "%0.2fMB/%0.2fMB" % (downloaded_size / 1024 / 1024, downloaded_size / 1024 / 1024)
|
|
54
|
+
progress.advance(song_progress_id, len(chunk))
|
|
55
|
+
progress.update(song_progress_id, description=f"{self.source}.download >>> {song_info['song_name']} (Downloading: {downloading_text})")
|
|
56
|
+
progress.update(song_progress_id, description=f"{self.source}.download >>> {song_info['song_name']} (Success)")
|
|
57
|
+
downloaded_song_infos.append(SongInfoUtils.fillsongtechinfo(copy.deepcopy(song_info), logger_handle=self.logger_handle, disable_print=self.disable_print))
|
|
58
|
+
except Exception as err:
|
|
59
|
+
progress.update(song_progress_id, description=f"{self.source}.download >>> {song_info['song_name']} (Error: {err})")
|
|
60
|
+
return downloaded_song_infos
|
|
61
|
+
'''_searchwithytsearchanddownloadmp3rapidapi'''
|
|
62
|
+
def _searchwithytsearchanddownloadmp3rapidapi(self, keyword: str, request_overrides: dict = None):
|
|
63
|
+
# init
|
|
64
|
+
request_overrides = request_overrides or {}
|
|
65
|
+
default_headers = {
|
|
66
|
+
"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",
|
|
67
|
+
"X-Rapidapi-Host": "youtube-music-api3.p.rapidapi.com", "X-Rapidapi-Key": random.choice(REPAIDAPI_KEYS),
|
|
68
|
+
"Referer": "https://music-download-lake.vercel.app/", "Origin": "https://music-download-lake.vercel.app",
|
|
69
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
70
|
+
}
|
|
71
|
+
# search
|
|
72
|
+
params = {'q': keyword, 'type': 'song', 'limit': self.search_size_per_source}
|
|
73
|
+
for repaidapi_key in REPAIDAPI_KEYS:
|
|
74
|
+
try:
|
|
75
|
+
default_headers['X-Rapidapi-Key'] = repaidapi_key
|
|
76
|
+
resp = self.get(f'https://youtube-music-api3.p.rapidapi.com/search', params=params, headers=default_headers, **request_overrides)
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
search_results = resp2json(resp=resp)['result']
|
|
79
|
+
assert len(search_results) > 0
|
|
80
|
+
break
|
|
81
|
+
except:
|
|
82
|
+
continue
|
|
83
|
+
# return
|
|
84
|
+
return search_results
|
|
85
|
+
'''_parsewithytsearchanddownloadmp3rapidapi'''
|
|
86
|
+
def _parsewithytsearchanddownloadmp3rapidapi(self, search_result: dict, request_overrides: dict = None):
|
|
87
|
+
# init
|
|
88
|
+
request_overrides, vid = request_overrides or {}, search_result['videoId']
|
|
89
|
+
default_headers = {
|
|
90
|
+
"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",
|
|
91
|
+
"X-Rapidapi-Host": "yt-search-and-download-mp3.p.rapidapi.com", "X-Rapidapi-Key": random.choice(REPAIDAPI_KEYS),
|
|
92
|
+
"Referer": "https://music-download-lake.vercel.app/", "Origin": "https://music-download-lake.vercel.app",
|
|
93
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
94
|
+
}
|
|
95
|
+
# parse
|
|
96
|
+
for repaidapi_key in REPAIDAPI_KEYS:
|
|
97
|
+
try:
|
|
98
|
+
self.default_headers['X-Rapidapi-Key'] = repaidapi_key
|
|
99
|
+
resp = self.get(f'https://yt-search-and-download-mp3.p.rapidapi.com/mp3?url=https://www.youtube.com/watch?v={vid}', headers=default_headers, **request_overrides)
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
download_result = resp2json(resp=resp)
|
|
102
|
+
except:
|
|
103
|
+
continue
|
|
104
|
+
if not download_result.get('download'): continue
|
|
105
|
+
song_info = SongInfo(
|
|
106
|
+
source=self.source, download_url=download_result['download'], download_url_status=self.audio_link_tester.test(download_result['download'], request_overrides),
|
|
107
|
+
ext='mp3', raw_data={'search': search_result, 'download': download_result}, duration=search_result.get('duration')
|
|
108
|
+
)
|
|
109
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides)
|
|
110
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
111
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
112
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
113
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
114
|
+
if song_info.with_valid_download_url: break
|
|
115
|
+
# return
|
|
116
|
+
return song_info
|
|
117
|
+
'''_parsvidewithmp3youtube'''
|
|
118
|
+
def _parsvidewithmp3youtube(self, search_result: dict, request_overrides: dict = None):
|
|
119
|
+
# init
|
|
120
|
+
request_overrides, vid = request_overrides or {}, search_result['videoId']
|
|
121
|
+
request_headers = {"Content-Type": "application/json", "Origin": "https://iframe.y2meta-uk.com", "Accept": "*/*"}
|
|
122
|
+
download_headers = {"Content-Type": "application/json", "Origin": "https://iframe.y2meta-uk.com", "Accept": "*/*", "Referer": "https://iframe.y2meta-uk.com/"}
|
|
123
|
+
request_overrides_test = copy.deepcopy(request_overrides)
|
|
124
|
+
request_overrides_test['headers'] = download_headers
|
|
125
|
+
# get key
|
|
126
|
+
resp = self.get('https://api.mp3youtube.cc/v2/sanity/key', headers=request_headers, **request_overrides)
|
|
127
|
+
resp.raise_for_status()
|
|
128
|
+
# parse
|
|
129
|
+
request_headers.update(dict(key=resp2json(resp)['key']))
|
|
130
|
+
for quality in ['320', '256', '192', '128', '64']:
|
|
131
|
+
audio_payload = {"link": f"https://youtu.be/{vid}", "format": "mp3", "audioBitrate": quality, "videoQuality": "720", "vCodec": "h264"}
|
|
132
|
+
try:
|
|
133
|
+
resp = self.post('https://api.mp3youtube.cc/v2/converter', json=audio_payload, headers=request_headers, **request_overrides)
|
|
134
|
+
resp.raise_for_status()
|
|
135
|
+
except:
|
|
136
|
+
continue
|
|
137
|
+
download_result = resp2json(resp=resp)
|
|
138
|
+
if not download_result.get('url'): continue
|
|
139
|
+
song_info = SongInfo(
|
|
140
|
+
source=self.source, default_download_headers=download_headers, download_url=download_result['url'],
|
|
141
|
+
download_url_status=self.audio_link_tester.test(download_result['url'], request_overrides_test), ext='mp3',
|
|
142
|
+
raw_data={'search': search_result, 'download': download_result}, duration=search_result.get('duration')
|
|
143
|
+
)
|
|
144
|
+
song_info.download_url_status['probe_status'] = self.audio_link_tester.probe(song_info.download_url, request_overrides_test)
|
|
145
|
+
ext, file_size = song_info.download_url_status['probe_status']['ext'], song_info.download_url_status['probe_status']['file_size']
|
|
146
|
+
if file_size and file_size != 'NULL': song_info.file_size = file_size
|
|
147
|
+
if not song_info.file_size: song_info.file_size = 'NULL'
|
|
148
|
+
if ext and ext != 'NULL': song_info.ext = ext
|
|
149
|
+
if song_info.with_valid_download_url: break
|
|
150
|
+
# return
|
|
151
|
+
return song_info
|
|
152
|
+
'''_constructsearchurls'''
|
|
153
|
+
def _constructsearchurls(self, keyword: str, rule: dict = None, request_overrides: dict = None):
|
|
154
|
+
# init
|
|
155
|
+
rule, request_overrides = rule or {}, request_overrides or {}
|
|
156
|
+
# adapt ytmusicapi to conduct music file search
|
|
157
|
+
proxies = None
|
|
158
|
+
if self.proxied_session_client is not None: proxies = self.proxied_session_client.getrandomproxy()
|
|
159
|
+
ytmusic_api = YTMusic(
|
|
160
|
+
auth=rule.get('auth', None), user=rule.get('user', None), requests_session=None, proxies=request_overrides.get('proxies', None) or proxies,
|
|
161
|
+
language=rule.get('language', 'en'), location=rule.get('location', ''), oauth_credentials=rule.get('oauth_credentials', ''),
|
|
162
|
+
)
|
|
163
|
+
# search rules
|
|
164
|
+
search_rule = {
|
|
165
|
+
'query': keyword, 'filter': rule.get('filter', None), 'scope': rule.get('scope', None), 'limit': self.search_size_per_source,
|
|
166
|
+
'ignore_spelling': rule.get('ignore_spelling', False)
|
|
167
|
+
}
|
|
168
|
+
search_urls = [{'ytmusic_api': ytmusic_api, 'search_rule': search_rule}]
|
|
169
|
+
self.search_size_per_page = self.search_size_per_source
|
|
170
|
+
# return
|
|
171
|
+
return search_urls
|
|
172
|
+
'''_search'''
|
|
173
|
+
@usesearchheaderscookies
|
|
174
|
+
def _search(self, keyword: str = '', search_url: dict = {}, request_overrides: dict = None, song_infos: list = [], progress: Progress = None, progress_id: int = 0):
|
|
175
|
+
# init
|
|
176
|
+
request_overrides = request_overrides or {}
|
|
177
|
+
search_meta = copy.deepcopy(search_url)
|
|
178
|
+
ytmusic_api, search_rule = search_meta['ytmusic_api'], search_meta['search_rule']
|
|
179
|
+
# successful
|
|
180
|
+
try:
|
|
181
|
+
# --search results, if search scale not very large, try rapid api first
|
|
182
|
+
if self.search_size_per_source <= 10:
|
|
183
|
+
try:
|
|
184
|
+
search_results = self._searchwithytsearchanddownloadmp3rapidapi(keyword=keyword, request_overrides=request_overrides)
|
|
185
|
+
if len(search_results) == 0: raise
|
|
186
|
+
except:
|
|
187
|
+
search_results = ytmusic_api.search(**search_rule)
|
|
188
|
+
search_results = [s for s in search_results if s['resultType'] in ['song']]
|
|
189
|
+
else:
|
|
190
|
+
search_results = ytmusic_api.search(**search_rule)
|
|
191
|
+
search_results = [s for s in search_results if s['resultType'] in ['song']]
|
|
192
|
+
for search_result in search_results:
|
|
193
|
+
# --download results
|
|
194
|
+
if not isinstance(search_result, dict) or 'videoId' not in search_result:
|
|
195
|
+
continue
|
|
196
|
+
song_info, ump_success = SongInfo(source=self.source), False
|
|
197
|
+
# ----download with UMP
|
|
198
|
+
try:
|
|
199
|
+
download_url = YouTube(video_id=search_result['videoId']).streams.getaudioonly()
|
|
200
|
+
song_info = SongInfo(
|
|
201
|
+
source=self.source, download_url=download_url, download_url_status={'ok': True}, file_size=byte2mb(download_url.filesize),
|
|
202
|
+
file_size_bytes=download_url.filesize, duration_s=int(download_url.durationMs) // 1000, duration=seconds2hms(int(download_url.durationMs) // 1000),
|
|
203
|
+
ext='mp3', raw_data={'search': search_result, 'download': {}},
|
|
204
|
+
)
|
|
205
|
+
if song_info.file_size_bytes < 100: raise
|
|
206
|
+
ump_success = True
|
|
207
|
+
except:
|
|
208
|
+
ump_success = False
|
|
209
|
+
pass
|
|
210
|
+
# ----parse with _parsvidewithmp3youtube
|
|
211
|
+
if not ump_success:
|
|
212
|
+
song_info = self._parsvidewithmp3youtube(search_result=search_result, request_overrides=request_overrides)
|
|
213
|
+
# ----parse with _parsewithytsearchanddownloadmp3rapidapi
|
|
214
|
+
if (not ump_success) and (not song_info.with_valid_download_url):
|
|
215
|
+
song_info = self._parsewithytsearchanddownloadmp3rapidapi(search_result=search_result, request_overrides=request_overrides)
|
|
216
|
+
# ----parse more information
|
|
217
|
+
format_duration = lambda d: "{:02}:{:02}:{:02}".format(*([0] * (3 - len(d.split(":"))) + list(map(int, d.split(":")))))
|
|
218
|
+
song_info.duration = format_duration(song_info.duration)
|
|
219
|
+
singers = ', '.join([singer.get('name', 'NULL') for singer in search_result.get('artists', [])]) if search_result.get('artists') else search_result.get('author', 'NULL')
|
|
220
|
+
song_info.update(dict(
|
|
221
|
+
singers=legalizestring(singers, replace_null_string='NULL'), song_name=legalizestring(search_result.get('title', 'NULL'), replace_null_string='NULL'),
|
|
222
|
+
album=legalizestring(search_result.get('album', 'NULL'), replace_null_string='NULL'), identifier=search_result['videoId'],
|
|
223
|
+
))
|
|
224
|
+
# --lyric results
|
|
225
|
+
lyric_result, lyric = dict(), 'NULL'
|
|
226
|
+
song_info.raw_data['lyric'] = lyric_result
|
|
227
|
+
song_info.lyric = lyric
|
|
228
|
+
# --append to song_infos
|
|
229
|
+
song_infos.append(song_info)
|
|
230
|
+
# --judgement for search_size
|
|
231
|
+
if self.strict_limit_search_size_per_page and len(song_infos) >= self.search_size_per_page: break
|
|
232
|
+
# --update progress
|
|
233
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Success)")
|
|
234
|
+
# failure
|
|
235
|
+
except Exception as err:
|
|
236
|
+
progress.update(progress_id, description=f"{self.source}.search >>> {search_url} (Error: {err})")
|
|
237
|
+
# return
|
|
238
|
+
return song_infos
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
'''
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
4
|
-
from .
|
|
1
|
+
'''initialize'''
|
|
2
|
+
from .data import SongInfo
|
|
3
|
+
from .lyric import WhisperLRC
|
|
4
|
+
from .quarkparser import QuarkParser
|
|
5
|
+
from .songinfoutils import SongInfoUtils
|
|
6
|
+
from .modulebuilder import BaseModuleBuilder
|
|
7
|
+
from .logger import LoggerHandle, colorize, printtable, printfullline, smarttrunctable
|
|
8
|
+
from .misc import (
|
|
9
|
+
AudioLinkTester, legalizestring, touchdir, seconds2hms, byte2mb, cachecookies, resp2json, isvalidresp, safeextractfromdict, replacefile,
|
|
10
|
+
usedownloadheaderscookies, useparseheaderscookies, usesearchheaderscookies, cookies2dict, cookies2string, estimatedurationwithfilesizebr,
|
|
11
|
+
estimatedurationwithfilelink
|
|
12
|
+
)
|