Skip to content

Commit a7acad1

Browse files
committed
Version 0.1.3
apis: Implement `LoginViaCookie`
1 parent db8a8e9 commit a7acad1

File tree

2 files changed

+107
-72
lines changed

2 files changed

+107
-72
lines changed

pyncm_async/__init__.py

+28-21
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"""PyNCM-Async 网易云音乐 Python 异步 API / 下载工具"""
33
__VERSION_MAJOR__ = 0
44
__VERSION_MINOR__ = 1
5-
__VERSION_PATCH__ = 2
5+
__VERSION_PATCH__ = 3
66

7-
__version__ = '%s.%s.%s' % (__VERSION_MAJOR__,__VERSION_MINOR__,__VERSION_PATCH__)
7+
__version__ = "%s.%s.%s" % (__VERSION_MAJOR__, __VERSION_MINOR__, __VERSION_PATCH__)
88

99

1010
from threading import current_thread
@@ -17,12 +17,13 @@
1717
import httpx
1818

1919
logger = logging.getLogger("pyncm.api")
20-
if 'PYNCM_DEBUG' in os.environ:
21-
debug_level = os.environ['PYNCM_DEBUG'].upper()
22-
if not debug_level in {'CRITICAL', 'DEBUG', 'ERROR', 'FATAL', 'INFO', 'WARNING'}:
23-
debug_level = 'DEBUG'
24-
logging.basicConfig(level=debug_level,
25-
format="[%(levelname).4s] %(name)s %(message)s")
20+
if "PYNCM_DEBUG" in os.environ:
21+
debug_level = os.environ["PYNCM_DEBUG"].upper()
22+
if not debug_level in {"CRITICAL", "DEBUG", "ERROR", "FATAL", "INFO", "WARNING"}:
23+
debug_level = "DEBUG"
24+
logging.basicConfig(
25+
level=debug_level, format="[%(levelname).4s] %(name)s %(message)s"
26+
)
2627

2728
DEVICE_ID_DEFAULT = "pyncm!"
2829
# This sometimes fails with some strings, for no particular reason. Though `pyncm!` seem to work everytime..?
@@ -50,7 +51,7 @@ class Session(httpx.AsyncClient):
5051
5152
```python
5253
# 利用全局 Session 完成该 API Call
53-
await LoginViaEmail(...)
54+
await LoginViaEmail(...)
5455
async with CreateNewSession(): # 建立新的 Session,并进入该 Session, 在 `with` 内的 API 将由该 Session 完成
5556
await LoginViaCellPhone(...)
5657
# 离开 Session. 此后 API 将继续由全局 Session 管理
@@ -61,9 +62,12 @@ class Session(httpx.AsyncClient):
6162
6263
获取其他具体信息请参考该文档注释
6364
"""
65+
6466
HOST = "music.163.com"
6567
# 网易云音乐 API 服务器域名,可直接改为代理服务器之域名
66-
UA_DEFAULT = "Mozilla/5.0 ([email protected]/mos9527/pyncm) Chrome/PyNCM.%s" % __version__
68+
UA_DEFAULT = (
69+
"Mozilla/5.0 ([email protected]/mos9527/pyncm) Chrome/PyNCM.%s" % __version__
70+
)
6771
# Weapi 使用的 UA
6872
UA_EAPI = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/2.10.2.200154"
6973
# EAPI 使用的 UA,不推荐更改
@@ -88,11 +92,7 @@ def __init__(self, *args, **kwargs):
8892
"User-Agent": self.UA_DEFAULT,
8993
"Referer": self.HOST,
9094
}
91-
self.login_info = {
92-
"success": False,
93-
"tick": time(),
94-
"content": None
95-
}
95+
self.login_info = {"success": False, "tick": time(), "content": None}
9696
self.eapi_config = {
9797
"os": "ios",
9898
"appver": "9.0.0",
@@ -180,9 +180,8 @@ async def request(
180180
),
181181
"cookies": (
182182
lambda self: [
183-
{"name": c.name, "value": c.value,
184-
"domain": c.domain, "path": c.path}
185-
for c in getattr(getattr(self, "cookies"),"jar")
183+
{"name": c.name, "value": c.value, "domain": c.domain, "path": c.path}
184+
for c in getattr(getattr(self, "cookies"), "jar")
186185
],
187186
lambda self, cookies: [
188187
getattr(self, "cookies").set(**cookie) for cookie in cookies
@@ -203,6 +202,7 @@ def load(self, dumped):
203202
self._session_info[k][1](self, v)
204203
return True
205204

205+
206206
class SessionManager:
207207
"""PyNCM Session 单例储存对象"""
208208

@@ -217,7 +217,8 @@ def get(self):
217217
def set(self, session):
218218
if SESSION_STACK.get(current_thread(), None):
219219
raise Exception(
220-
"Current Session is in `with` block, which cannot be reassigned.")
220+
"Current Session is in `with` block, which cannot be reassigned."
221+
)
221222
self.session = session
222223

223224
# Session serialization
@@ -243,23 +244,29 @@ def stringify(session: Session) -> str:
243244
from json import dumps
244245
from zlib import compress
245246
from base64 import b64encode
246-
return 'PYNCM' + b64encode(compress(dumps(session.dump()).encode())).decode()
247+
248+
return "PYNCM" + b64encode(compress(dumps(session.dump()).encode())).decode()
247249

248250
@staticmethod
249251
def parse(dump: str) -> Session:
250252
"""反序列化 `str` 为 `Session`"""
251-
if dump[:5] == 'PYNCM': # New marshaler (compressed,base64 encoded) has magic header
253+
if (
254+
dump[:5] == "PYNCM"
255+
): # New marshaler (compressed,base64 encoded) has magic header
252256
from json import loads
253257
from zlib import decompress
254258
from base64 import b64decode
259+
255260
session = Session()
256261
session.load(loads(decompress(b64decode(dump[5:])).decode()))
257262
return session
258263
else:
259264
return SessionManager.parse_legacy(dump)
260265

266+
261267
sessionManager = SessionManager()
262268

269+
263270
def GetCurrentSession() -> Session:
264271
"""获取当前正在被 PyNCM 使用的 Session / 登录态"""
265272
return sessionManager.get()

pyncm_async/apis/login.py

+79-51
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def WriteLoginInfo(response, session):
2727
session.login_info["success"] = False
2828
raise LoginFailedException(session.login_info["content"])
2929
session.login_info["success"] = True
30-
session.csrf_token = session.cookies.get('__csrf')
30+
session.csrf_token = session.cookies.get("__csrf")
3131

3232

3333
@WeapiCryptoRequest
@@ -40,14 +40,14 @@ def LoginLogout():
4040
return "/weapi/logout", {}
4141

4242

43-
@WeapiCryptoRequest
43+
@EapiCryptoRequest
4444
def LoginRefreshToken():
4545
"""网页端 - 刷新登录令牌
4646
4747
Returns:
4848
dict
4949
"""
50-
return "/weapi/w/login/cellphone", {}
50+
return "/eapi/login/token/refresh", {}
5151

5252

5353
@WeapiCryptoRequest
@@ -102,7 +102,31 @@ def GetCurrentLoginStatus():
102102
return "/weapi/w/nuser/account/get", {}
103103

104104

105-
async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ctcode=86, remeberLogin=True, session=None) -> dict:
105+
async def LoginViaCookie(MUSIC_U="", **kwargs):
106+
"""通过 Cookie 登陆
107+
108+
Args:
109+
MUSIC_U (str, optional): Cookie 中的 MUSIC_U. Defaults to ''.
110+
111+
Returns:
112+
dict
113+
"""
114+
session = GetCurrentSession()
115+
session.cookies.update({"MUSIC_U": MUSIC_U, **kwargs})
116+
resp = await GetCurrentLoginStatus()
117+
WriteLoginInfo(resp, session)
118+
return {"code": 200, "result": session.login_info}
119+
120+
121+
async def LoginViaCellphone(
122+
phone="",
123+
password="",
124+
passwordHash="",
125+
captcha="",
126+
ctcode=86,
127+
remeberLogin=True,
128+
session=None,
129+
) -> dict:
106130
"""PC 端 - 手机号登陆
107131
108132
* 若同时指定 password 和 passwordHash, 优先使用 password
@@ -114,8 +138,8 @@ async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ct
114138
remeberLogin (bool, optional): 是否‘自动登录’,设置 `False` 可能导致权限问题. Defaults to True.
115139
* 以下验证方式有 1 个含参即可
116140
password (str, optional): 明文密码. Defaults to ''.
117-
passwordHash (str, optional): 密码md5哈希. Defaults to ''.
118-
captcha (str, optional): 手机验证码. 需要已在同一 Session 中发送过 SetSendRegisterVerifcationCodeViaCellphone. Defaults to ''.
141+
passwordHash (str, optional): 密码md5哈希. Defaults to ''.
142+
captcha (str, optional): 手机验证码. 需要已在同一 Session 中发送过 SetSendRegisterVerifcationCodeViaCellphone. Defaults to ''.
119143
120144
Raises:
121145
LoginFailedException: 登陆失败时发生
@@ -126,43 +150,47 @@ async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ct
126150
path = "/eapi/w/login/cellphone"
127151
session = session or GetCurrentSession()
128152
if password:
129-
passwordHash = HashHexDigest(password)
130-
153+
passwordHash = HashHexDigest(password)
154+
131155
if not (passwordHash or captcha):
132156
raise LoginFailedException("未提供密码或验证码")
133157

134-
auth_token = {"password": str(passwordHash)} if not captcha else {"captcha": str(captcha)}
158+
auth_token = (
159+
{"password": str(passwordHash)} if not captcha else {"captcha": str(captcha)}
160+
)
135161

136162
login_status = await EapiCryptoRequest(
137163
lambda: (
138164
path,
139165
{
140-
"type": '1',
141-
"phone": str(phone),
166+
"type": "1",
167+
"phone": str(phone),
142168
"remember": str(remeberLogin).lower(),
143169
"countrycode": str(ctcode),
144-
"checkToken" : "",
145-
**auth_token
170+
"checkToken": "",
171+
**auth_token,
146172
},
147173
)
148174
)(session=session)
149-
175+
150176
WriteLoginInfo(login_status, session)
151-
return {'code':200,'result':session.login_info}
177+
return {"code": 200, "result": session.login_info}
152178

153179

154-
async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True, session=None) -> dict:
180+
async def LoginViaEmail(
181+
email="", password="", passwordHash="", remeberLogin=True, session=None
182+
) -> dict:
155183
"""网页端 - 邮箱登陆
156184
157185
* 若同时指定 password 和 passwordHash, 优先使用 password
158-
186+
159187
Args:
160188
email (str, optional): 邮箱地址. Defaults to ''.
161189
remeberLogin (bool, optional): 是否‘自动登录’,设置 `False` 可能导致权限问题. Defaults to True.
162190
* 以下验证方式有 1 个含参即可
163191
password (str, optional): 明文密码. Defaults to ''.
164-
passwordHash (str, optional): 密码md5哈希. Defaults to ''.
165-
192+
passwordHash (str, optional): 密码md5哈希. Defaults to ''.
193+
166194
Raises:
167195
LoginFailedException: 登陆失败时发生
168196
@@ -172,8 +200,8 @@ async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True
172200
path = "/eapi/login"
173201
session = session or GetCurrentSession()
174202
if password:
175-
passwordHash = HashHexDigest(password)
176-
203+
passwordHash = HashHexDigest(password)
204+
177205
if not passwordHash:
178206
raise LoginFailedException("未提供密码")
179207

@@ -183,56 +211,55 @@ async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True
183211
lambda: (
184212
path,
185213
{
186-
"type": '1',
187-
"username": str(email),
188-
"remember": str(remeberLogin).lower(),
189-
**auth_token
214+
"type": "1",
215+
"username": str(email),
216+
"remember": str(remeberLogin).lower(),
217+
**auth_token,
190218
},
191219
)
192220
)(session=session)
193-
194-
WriteLoginInfo(login_status,session)
195-
return {'code':200,'result':session.login_info}
221+
222+
WriteLoginInfo(login_status, session)
223+
return {"code": 200, "result": session.login_info}
224+
196225

197226
async def LoginViaAnonymousAccount(deviceId=None, session=None):
198-
'''PC 端 - 游客登陆
227+
"""PC 端 - 游客登陆
199228
200229
Args:
201230
deviceId (str optional): 设备 ID. 设置非 None 将同时改变 Session 的设备 ID. Defaults to None.
202-
231+
203232
Notes:
204233
Session 默认使用 `pyncm!` 作为设备 ID
205234
206235
Returns:
207236
dict
208-
'''
237+
"""
209238
session = session or GetCurrentSession()
210239
if not deviceId:
211-
deviceId = session.deviceId
240+
deviceId = session.deviceId
212241
login_status = WeapiCryptoRequest(
213-
lambda: ("/api/register/anonimous" , {
214-
"username" : b64encode(
215-
('%s %s' % (
216-
deviceId,
217-
cloudmusic_dll_encode_id(deviceId))).encode()
218-
).decode()
219-
}
242+
lambda: (
243+
"/api/register/anonimous",
244+
{
245+
"username": b64encode(
246+
("%s %s" % (deviceId, cloudmusic_dll_encode_id(deviceId))).encode()
247+
).decode()
248+
},
220249
)
221-
)(session=session)
222-
assert login_status['code'] == 200,"匿名登陆失败"
223-
WriteLoginInfo({
224-
**login_status,
225-
'profile':{
226-
'nickname' : 'Anonymous',
227-
**login_status
250+
)(session=session)
251+
assert login_status["code"] == 200, "匿名登陆失败"
252+
WriteLoginInfo(
253+
{
254+
**login_status,
255+
"profile": {"nickname": "Anonymous", **login_status},
256+
"account": {"id": login_status["userId"], **login_status},
228257
},
229-
'account':{
230-
'id' : login_status['userId'],
231-
**login_status
232-
}
233-
}, session)
258+
session,
259+
)
234260
return session.login_info
235261

262+
236263
@WeapiCryptoRequest
237264
def SetSendRegisterVerifcationCodeViaCellphone(cell: str, ctcode=86):
238265
"""网页端 - 发送验证码
@@ -294,6 +321,7 @@ def SetRegisterAccountViaCellphone(
294321
"phone": str(cell),
295322
}
296323

324+
297325
@EapiCryptoRequest
298326
def CheckIsCellphoneRegistered(cell: str, prefix=86):
299327
"""移动端 - 检查某手机号是否已注册

0 commit comments

Comments
 (0)