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,184 @@
1
+ '''
2
+ Function:
3
+ Implementation of QQMusicClient utils
4
+ Author:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
+ Charles的皮卡丘
8
+ '''
9
+ import time
10
+ import orjson
11
+ import base64
12
+ import random
13
+ import string
14
+ import hashlib
15
+ import requests
16
+ import binascii
17
+ from uuid import uuid4
18
+ from typing import ClassVar, cast
19
+ from datetime import datetime, timedelta
20
+ from dataclasses import dataclass, field
21
+ from cryptography.hazmat.primitives import serialization
22
+ from cryptography.hazmat.primitives.asymmetric import padding
23
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
24
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
25
+
26
+
27
+ '''constants'''
28
+ PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
29
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEIxgwoutfwoJxcGQeedgP7FG9qaIuS0qzfR8gWkrkTZKM2iWHn2ajQpBRZjMSoSf6+KJGvar2ORhBfpDXyVtZCKpqLQ+FLkpncClKVIrBwv6PHyUvuCb0rIarmgDnzkfQAqVufEtR64iazGDKatvJ9y6B9NMbHddGSAUmRTCrHQIDAQAB
30
+ -----END PUBLIC KEY-----"""
31
+ SECRET = "ZdJqM15EeO2zWc08"
32
+ APP_KEY = "0AND0HD6FE4HY80F"
33
+ DEFAULT_VIP_QUALITIES = {
34
+ "MASTER": ("AIM0", ".mflac"), "ATMOS_2": ("Q0M0", ".mflac"), "ATMOS_51": ("Q0M1", ".mflac"), "FLAC": ("F0M0", ".mflac"),
35
+ "OGG_640": ("O801", ".mgg"), "OGG_320": ("O800", ".mgg"), "OGG_192": ("O6M0", ".mgg"), "OGG_96": ("O4M0", ".mgg"),
36
+ }
37
+ DEFAULT_QUALITIES = {
38
+ "MASTER": ("AI00", ".flac"), "ATMOS_2": ("Q000", ".flac"), "ATMOS_51": ("Q001", ".flac"), "FLAC": ("F000", ".flac"),
39
+ "OGG_640": ("O801", ".ogg"), "OGG_320": ("O800", ".ogg"), "OGG_192": ("O600", ".ogg"), "OGG_96": ("O400", ".ogg"),
40
+ "MP3_320": ("M800", ".mp3"), "MP3_128": ("M500", ".mp3"), "ACC_192": ("C600", ".m4a"), "ACC_96": ("C400", ".m4a"),
41
+ "ACC_48": ("C200", ".m4a"),
42
+ }
43
+
44
+
45
+ '''QQMusicClientUtils'''
46
+ class QQMusicClientUtils(object):
47
+ '''randomimei'''
48
+ @staticmethod
49
+ def randomimei():
50
+ imei, sum_ = [], 0
51
+ for i in range(14):
52
+ num = random.randint(0, 9)
53
+ if (i + 2) % 2 == 0:
54
+ num *= 2
55
+ if num >= 10: num = (num % 10) + 1
56
+ sum_ += num
57
+ imei.append(str(num))
58
+ ctrl_digit = (sum_ * 9) % 10
59
+ imei.append(str(ctrl_digit))
60
+ return "".join(imei)
61
+ '''rsaencrypt'''
62
+ @staticmethod
63
+ def rsaencrypt(content: bytes):
64
+ key = cast(RSAPublicKey, serialization.load_pem_public_key(PUBLIC_KEY.encode()))
65
+ return key.encrypt(content, padding.PKCS1v15())
66
+ '''aesencrypt'''
67
+ @staticmethod
68
+ def aesencrypt(key: bytes, content: bytes):
69
+ cipher = Cipher(algorithms.AES(key), modes.CBC(key))
70
+ padding_size = 16 - len(content) % 16
71
+ encryptor = cipher.encryptor()
72
+ return encryptor.update(content + (padding_size * chr(padding_size)).encode()) + encryptor.finalize()
73
+ '''calcmd5'''
74
+ @staticmethod
75
+ def calcmd5(*strings: str | bytes):
76
+ md5 = hashlib.md5()
77
+ for item in strings:
78
+ assert isinstance(item, (str, bytes))
79
+ if isinstance(item, bytes): md5.update(item)
80
+ elif isinstance(item, str): md5.update(item.encode())
81
+ return md5.hexdigest()
82
+ '''randombeaconid'''
83
+ @staticmethod
84
+ def randombeaconid():
85
+ beacon_id = ""
86
+ time_month = datetime.now().strftime("%Y-%m-") + "01"
87
+ rand1 = random.randint(100000, 999999)
88
+ rand2 = random.randint(100000000, 999999999)
89
+ for i in range(1, 41):
90
+ if i in [1, 2, 13, 14, 17, 18, 21, 22, 25, 26, 29, 30, 33, 34, 37, 38]:
91
+ beacon_id += f"k{i}:{time_month}{rand1}.{rand2}"
92
+ elif i == 3:
93
+ beacon_id += "k3:0000000000000000"
94
+ elif i == 4:
95
+ beacon_id += f"k4:{''.join(random.choices('123456789abcdef', k=16))}"
96
+ else:
97
+ beacon_id += f"k{i}:{random.randint(0, 9999)}"
98
+ beacon_id += ";"
99
+ return beacon_id
100
+ '''randompayloadbydevice'''
101
+ @staticmethod
102
+ def randompayloadbydevice(device, version: str):
103
+ fixed_rand = random.randint(0, 14400)
104
+ reserved = {
105
+ "harmony": "0", "clone": "0", "containe": "", "oz": "UhYmelwouA+V2nPWbOvLTgN2/m8jwGB+yUB5v9tysQg=", "oo": "Xecjt+9S1+f8Pz2VLSxgpw==",
106
+ "kelong": "0", "uptimes": (datetime.now() - timedelta(seconds=fixed_rand)).strftime("%Y-%m-%d %H:%M:%S"), "multiUser": "0",
107
+ "bod": device.brand, "dv": device.device, "firstLevel": "", "manufact": device.brand, "name": device.model, "host": "se.infra",
108
+ "kernel": device.proc_version,
109
+ }
110
+ return {
111
+ "androidId": device.android_id, "platformId": 1, "appKey": APP_KEY, "appVersion": version, "beaconIdSrc": QQMusicClientUtils.randombeaconid(),
112
+ "brand": device.brand, "channelId": "10003505", "cid": "", "imei": device.imei, "imsi": "", "mac": "", "model": device.model, "networkType": "unknown",
113
+ "oaid": "", "osVersion": f"Android {device.version.release},level {device.version.sdk}", "qimei": "", "qimei36": "", "sdkVersion": "1.2.13.6",
114
+ "targetSdkVersion": "33", "audit": "", "userId": "{}", "packageId": "com.tencent.qqmusic", "deviceType": "Phone", "sdkName": "",
115
+ "reserved": orjson.dumps(reserved).decode(),
116
+ }
117
+ '''obtainqimei'''
118
+ @staticmethod
119
+ def obtainqimei(version: str, device):
120
+ try:
121
+ payload = QQMusicClientUtils.randompayloadbydevice(device, version)
122
+ crypt_key = "".join(random.choices("adbcdef1234567890", k=16))
123
+ nonce = "".join(random.choices("adbcdef1234567890", k=16))
124
+ ts = int(time.time())
125
+ key = base64.b64encode(QQMusicClientUtils.rsaencrypt(crypt_key.encode())).decode()
126
+ params = base64.b64encode(QQMusicClientUtils.aesencrypt(crypt_key.encode(), orjson.dumps(payload))).decode()
127
+ extra = '{"appKey":"' + APP_KEY + '"}'
128
+ sign = QQMusicClientUtils.calcmd5(key, params, str(ts * 1000), nonce, SECRET, extra)
129
+ resp = requests.post(
130
+ "https://api.tencentmusic.com/tme/trpc/proxy",
131
+ headers={
132
+ "Host": "api.tencentmusic.com", "method": "GetQimei", "service": "trpc.tme_datasvr.qimeiproxy.QimeiProxy", "appid": "qimei_qq_android",
133
+ "sign": QQMusicClientUtils.calcmd5("qimei_qq_androidpzAuCmaFAaFaHrdakPjLIEqKrGnSOOvH", str(ts)), "user-agent": "QQMusic", "timestamp": str(ts),
134
+ },
135
+ json={
136
+ "app": 0, "os": 1, "qimeiParams": {"key": key, "params": params, "time": str(ts), "nonce": nonce, "sign": sign, "extra": extra},
137
+ },
138
+ )
139
+ data = orjson.loads(orjson.loads(resp.content)["data"])["data"]
140
+ device.qimei = data["q36"]
141
+ result = {"q16": data["q16"], "q36": data["q36"]}
142
+ return result
143
+ except:
144
+ result = {"q16": "", "q36": "6c9d3cd110abca9b16311cee10001e717614"}
145
+ return result
146
+
147
+
148
+ '''OSVersion'''
149
+ @dataclass
150
+ class OSVersion:
151
+ incremental: str = "5891938"
152
+ release: str = "10"
153
+ codename: str = "REL"
154
+ sdk: int = 29
155
+
156
+
157
+ '''Device'''
158
+ @dataclass
159
+ class Device:
160
+ display: str = field(default_factory=lambda: f"QMAPI.{random.randint(100000, 999999)}.001")
161
+ product: str = "iarim"
162
+ device: str = "sagit"
163
+ board: str = "eomam"
164
+ model: str = "MI 6"
165
+ fingerprint: str = field(default_factory=lambda: f"xiaomi/iarim/sagit:10/eomam.200122.001/{random.randint(1000000, 9999999)}:user/release-keys")
166
+ boot_id: str = field(default_factory=lambda: str(uuid4()))
167
+ proc_version: str = field(default_factory=lambda: f"Linux 5.4.0-54-generic-{''.join(random.choices(string.ascii_letters + string.digits, k=8))} (android-build@google.com)")
168
+ imei: str = field(default_factory=QQMusicClientUtils.randomimei)
169
+ brand: str = "Xiaomi"
170
+ bootloader: str = "U-boot"
171
+ base_band: str = ""
172
+ version: OSVersion = field(default_factory=OSVersion)
173
+ sim_info: str = "T-Mobile"
174
+ os_type: str = "android"
175
+ mac_address: str = "00:50:56:C0:00:08"
176
+ ip_address: ClassVar[list[int]] = [10, 0, 1, 3]
177
+ wifi_bssid: str = "00:50:56:C0:00:08"
178
+ wifi_ssid: str = "<unknown ssid>"
179
+ imsi_md5: list[int] = field(default_factory=lambda: list(hashlib.md5(bytes([random.randint(0, 255) for _ in range(16)])).digest()))
180
+ android_id: str = field(default_factory=lambda: binascii.hexlify(bytes([random.randint(0, 255) for _ in range(8)])).decode("utf-8"))
181
+ apn: str = "wifi"
182
+ vendor_name: str = "MIUI"
183
+ vendor_os_name: str = "qmapi"
184
+ qimei: None | str = None
@@ -0,0 +1,105 @@
1
+ '''
2
+ Function:
3
+ Implementation of QuarkParser
4
+ Author:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
+ Charles的皮卡丘
8
+ '''
9
+ import time
10
+ import requests
11
+ from .misc import resp2json
12
+ from urllib.parse import urlparse
13
+
14
+
15
+ '''QuarkParser'''
16
+ class QuarkParser():
17
+ '''parsefromurl'''
18
+ @staticmethod
19
+ def parsefromurl(url: str, passcode: str = '', cookies: str | dict = '', max_tries: int = 3):
20
+ for _ in range(max_tries):
21
+ try:
22
+ download_result, download_url = QuarkParser._parsefromurl(url=url, passcode=passcode, cookies=cookies)
23
+ break
24
+ except:
25
+ download_result, download_url = {}, ""
26
+ return download_result, download_url
27
+ '''_parsefromurl'''
28
+ @staticmethod
29
+ def _parsefromurl(url: str, passcode: str = '', cookies: str | dict = ''):
30
+ # init
31
+ session, download_result = requests.Session(), {}
32
+ parsed_url = urlparse(url)
33
+ pwd_id = parsed_url.path.strip('/').split('/')[-1]
34
+ if cookies and isinstance(cookies, str): cookies = dict(item.split("=", 1) for item in cookies.split("; "))
35
+ headers = {
36
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Core/1.94.225.400 QQBrowser/12.2.5544.400',
37
+ 'origin': 'https://pan.quark.cn', 'referer': 'https://pan.quark.cn/', 'accept-language': 'zh-CN,zh;q=0.9',
38
+ }
39
+ # share/sharepage/token
40
+ json_data = {'pwd_id': pwd_id, 'passcode': passcode}
41
+ params = {'pr': 'ucpro', 'fr': 'pc', 'uc_param_str': '', '__dt': '596', '__t': f'{str(int(time.time() * 1000))}'}
42
+ resp = session.post('https://drive-h.quark.cn/1/clouddrive/share/sharepage/token', params=params, json=json_data, cookies=cookies, headers=headers)
43
+ resp.raise_for_status()
44
+ token_data = resp2json(resp=resp)
45
+ stoken = token_data['data']['stoken']
46
+ download_result['token_data'] = token_data
47
+ time.sleep(0.1)
48
+ # share/sharepage/detail
49
+ params = {
50
+ "pr": "ucpro", "fr": "pc", "uc_param_str": "", "ver": "2", "pwd_id": pwd_id, "stoken": stoken, "pdir_fid": "0", "force": "0",
51
+ "_page": "1", "_size": "50", "_fetch_banner": "1", "_fetch_share": "1", "fetch_relate_conversation": "1", "_fetch_total": "1",
52
+ "_sort": "file_type:asc,file_name:asc", "__dt": "1020", "__t": f"{int(time.time() * 1000)}"
53
+ }
54
+ resp = session.get('https://drive-h.quark.cn/1/clouddrive/share/sharepage/detail', params=params, cookies=cookies, headers=headers)
55
+ resp.raise_for_status()
56
+ detail_data = resp2json(resp=resp)
57
+ fid = detail_data["data"]["list"][0]["fid"]
58
+ share_fid_token = detail_data["data"]["list"][0]["share_fid_token"]
59
+ download_result['detail_data'] = detail_data
60
+ time.sleep(0.1)
61
+ # clouddrive/file/info/path_list
62
+ params = {"pr": "ucpro", "fr": "pc", "uc_param_str": "", "__dt": "1266", "__t": f"{int(time.time() * 1000)}"}
63
+ json_data = {"file_path": ["/来自:分享"]}
64
+ resp = session.post('https://drive-pc.quark.cn/1/clouddrive/file/info/path_list', params=params, json=json_data, cookies=cookies, headers=headers)
65
+ resp.raise_for_status()
66
+ path_list_data = resp2json(resp=resp)
67
+ to_pdir_fid = path_list_data["data"][0]["fid"]
68
+ download_result['path_list_data'] = path_list_data
69
+ time.sleep(0.1)
70
+ # share/sharepage/save
71
+ params = {"pr": "ucpro", "fr": "pc", "uc_param_str": "", "__dt": "5660", "__t": f"{int(time.time() * 1000)}"}
72
+ json_data = {"pwd_id": pwd_id, "stoken": stoken, "pdir_fid": "0", "to_pdir_fid": to_pdir_fid, "fid_list": [fid], "fid_token_list": [share_fid_token], "scene": "link"}
73
+ resp = session.post(url='https://drive-pc.quark.cn/1/clouddrive/share/sharepage/save', params=params, cookies=cookies, json=json_data, headers=headers)
74
+ resp.raise_for_status()
75
+ save_data = resp2json(resp=resp)
76
+ task_id = save_data['data']['task_id']
77
+ download_result['save_data'] = save_data
78
+ time.sleep(0.1)
79
+ # clouddrive/task
80
+ for retry_index in range(5):
81
+ try:
82
+ params = {'pr': 'ucpro', 'fr': 'pc', 'uc_param_str': '', 'task_id': task_id, 'retry_index': str(retry_index), '__dt': '6355', '__t': f'{str(int(time.time() * 1000))}'}
83
+ resp = session.get('https://drive-pc.quark.cn/1/clouddrive/task', params=params, cookies=cookies, headers=headers)
84
+ resp.raise_for_status()
85
+ task_data = resp2json(resp=resp)
86
+ fid_encrypt = task_data['data']['save_as']['save_as_top_fids'][0]
87
+ download_result['task_data'] = task_data
88
+ break
89
+ except:
90
+ time.sleep(0.1)
91
+ continue
92
+ # clouddrive/file/download
93
+ headers = {
94
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.56 Chrome/100.0.4896.160 Electron/18.3.5.12-a038f7b798 Safari/537.36 Channel/pckk_other_ch",
95
+ "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "accept-language": "zh-CN", "origin": "https://pan.quark.cn", "referer": "https://pan.quark.cn/",
96
+ }
97
+ params = {'pr': 'ucpro', 'fr': 'pc', 'uc_param_str': '', '__dt': '6743', '__t': f'{str(int(time.time() * 1000))}'}
98
+ json_data = {'fids': [fid_encrypt]}
99
+ resp = session.post('https://drive-pc.quark.cn/1/clouddrive/file/download', params=params, json=json_data, cookies=cookies, headers=headers)
100
+ resp.raise_for_status()
101
+ download_data = resp2json(resp=resp)
102
+ download_url = download_data["data"][0]["download_url"]
103
+ download_result['download_data'] = download_data
104
+ # return
105
+ return download_result, download_url
@@ -0,0 +1,54 @@
1
+ '''
2
+ Function:
3
+ Implementation of SongInfoUtils
4
+ Author:
5
+ Zhenchao Jin
6
+ WeChat Official Account (微信公众号):
7
+ Charles的皮卡丘
8
+ '''
9
+ import os
10
+ from pathlib import Path
11
+ from .data import SongInfo
12
+ from tinytag import TinyTag
13
+ from .lyric import WhisperLRC
14
+ from .logger import LoggerHandle
15
+ from .misc import seconds2hms, byte2mb
16
+
17
+
18
+ '''SongInfoUtils'''
19
+ class SongInfoUtils:
20
+ '''fillsongtechinfo'''
21
+ @staticmethod
22
+ def fillsongtechinfo(song_info: SongInfo, logger_handle: LoggerHandle, disable_print: bool) -> SongInfo:
23
+ path = Path(song_info.save_path)
24
+ # correct file size
25
+ size = path.stat().st_size
26
+ song_info.file_size_bytes = size
27
+ song_info.file_size = byte2mb(size=size)
28
+ # tinytag parse
29
+ try:
30
+ tag = TinyTag.get(str(path))
31
+ except Exception as err:
32
+ logger_handle.warning(f'SongInfoUtils.fillsongtechinfo >>> {str(path)} (Err: {err})', disable_print=disable_print)
33
+ return song_info
34
+ if tag.duration:
35
+ song_info.duration_s = int(round(tag.duration))
36
+ song_info.duration = seconds2hms(tag.duration)
37
+ if tag.bitrate:
38
+ song_info.bitrate = int(round(tag.bitrate))
39
+ if tag.samplerate:
40
+ song_info.samplerate = int(tag.samplerate)
41
+ if tag.channels:
42
+ song_info.channels = int(tag.channels)
43
+ if getattr(tag, "codec", None):
44
+ song_info.codec = tag.codec
45
+ elif getattr(tag, "extra", None) and isinstance(tag.extra, dict):
46
+ song_info.codec = tag.extra.get("codec") or tag.extra.get("mime-type")
47
+ # lyric
48
+ if os.environ.get('ENABLE_WHISPERLRC', 'False').lower() == 'true' and ((not song_info.lyric) or (song_info.lyric == 'NULL')):
49
+ lyric_result = WhisperLRC(model_size_or_path='small').fromfilepath(str(path))
50
+ lyric = lyric_result['lyric']
51
+ song_info.lyric = lyric
52
+ song_info.raw_data['lyric'] = lyric_result
53
+ # return
54
+ return song_info