Skip to content

Commit b5ecef3

Browse files
committed
Version 0.1.0
api: API `session=` 关键字支持 (#55)
1 parent 654c922 commit b5ecef3

File tree

10 files changed

+61
-153
lines changed

10 files changed

+61
-153
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ async with CreateNewSession(): # 建立新的 Session,并进入该 Session,
3333
# 离开 Session. 此后 API 将继续由全局 Session 管理
3434
await GetTrackComments(...)
3535
```
36+
使用 `with`...
3637
- 注:Session 各*线程*独立,各线程利用 `with` 设置的 Session 不互相影响
3738
- 注:Session 离开 `with` clause 时,**Session 会被销毁**,但不会影响全局 Session
3839
- 注:Session 生命周期细节请参阅 https://www.python-httpx.org/async/
3940

41+
同时,你也可以在 API Call 中 指定 Session
42+
```python
43+
await GetTrackComments(..., session=session)
44+
```
45+
4046
详见 [Session 说明](https://github.com/mos9527/pyncm/blob/async/pyncm/__init__.py#L35)
4147
## API 说明
4248
大部分 API 函数已经详细注释,可读性较高。推荐参阅 [API 源码](https://github.com/mos9527/pyncm/tree/async/pyncm) 获得支持

pyncm-async.tests.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import asyncio
22
import logging
33
import pyncm_async, pyncm_async.apis
4-
logging.basicConfig(level=0)
4+
logging.basicConfig(level=logging.WARNING)
5+
logging.getLogger('pyncm.api').setLevel(logging.DEBUG)
56
# Account from https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/test/login.test.js
67
pyncm_async.SetCurrentSession(
78
pyncm_async.LoadSessionFromString(

pyncm_async/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# -*- coding: utf-8 -*-
22
"""PyNCM-Async 网易云音乐 Python 异步 API / 下载工具"""
33
__VERSION_MAJOR__ = 0
4-
__VERSION_MINOR__ = 0
5-
__VERSION_PATCH__ = 3
4+
__VERSION_MINOR__ = 1
5+
__VERSION_PATCH__ = 0
66

77
__version__ = '%s.%s.%s' % (__VERSION_MAJOR__,__VERSION_MINOR__,__VERSION_PATCH__)
88

pyncm_async/apis/__init__.py

+20-55
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,32 @@ def _BaseWrapper(requestFunc):
5454
5555
实际使用请参考以下其他 Wrapper::
5656
57-
LoginRequiredApi
58-
UserIDBasedApi
5957
WeapiCryptoRequest
60-
LapiCryptoRequest
6158
EapiCryptoRequest
6259
"""
6360
@wraps(requestFunc)
6461
def apiWrapper(apiFunc):
6562
@wraps(apiFunc)
6663
async def wrapper(*a, **k):
64+
# HACK: 'session=' keyword support
65+
session = k.get("session", GetCurrentSession())
66+
# HACK: For now,wrapped functions will not have access to the session object
67+
if 'session' in k: del k['session']
68+
6769
ret = apiFunc(*a, **k)
6870
url, payload = ret[:2]
6971
method = ret[-1] if ret[-1] in ["POST", "GET"] else "POST"
70-
logger.debug('TYPE=%s API=%s.%s %s url=%s deviceId=%s payload=%s' % (
72+
logger.debug('TYPE=%s API=%s.%s %s url=%s deviceId=%s payload=%s session=0x%x' % (
7173
requestFunc.__name__.split('Crypto')[0].upper(),
7274
apiFunc.__module__,
73-
apiFunc,
75+
apiFunc.__name__,
7476
method,
7577
url,
76-
GetCurrentSession().deviceId,
77-
payload)
78+
session.deviceId,
79+
payload,
80+
id(session))
7881
)
79-
rsp = await requestFunc(url, payload, method)
82+
rsp = await requestFunc(session, url, payload, method)
8083
try:
8184
payload = rsp.text if isinstance(rsp, Response) else rsp
8285
payload = payload.decode() if not isinstance(payload, str) else payload
@@ -100,30 +103,6 @@ async def wrapper(*a, **k):
100103

101104
return apiWrapper
102105

103-
104-
def LoginRequiredApi(func):
105-
"""API 需要事先登录"""
106-
@wraps(func)
107-
def wrapper(*a, **k):
108-
if not GetCurrentSession().login_info["success"]:
109-
raise LOGIN_REQUIRED
110-
return func(*a, **k)
111-
112-
return wrapper
113-
114-
115-
def UserIDBasedApi(func):
116-
"""API 第一参数为用户 ID,而该参数可留 0 而指代已登录的用户 ID"""
117-
@wraps(func)
118-
def wrapper(user_id=0, *a, **k):
119-
if user_id == 0 and GetCurrentSession().login_info["success"]:
120-
user_id = GetCurrentSession().uid
121-
elif user_id == 0:
122-
raise LOGIN_REQUIRED
123-
return func(user_id, *a, **k)
124-
125-
return wrapper
126-
127106
def EapiEncipered(func):
128107
"""函数值有 Eapi 加密 - 解密并返回原文"""
129108
@wraps(func)
@@ -137,45 +116,31 @@ def wrapper(*a, **k):
137116
return wrapper
138117

139118
@_BaseWrapper
140-
async def WeapiCryptoRequest(url, plain, method):
119+
async def WeapiCryptoRequest(session, url, plain, method):
141120
"""Weapi - 适用于 网页端、小程序、手机端部分 APIs"""
142-
sess = GetCurrentSession()
143-
payload = json.dumps({**plain, "csrf_token": sess.csrf_token})
144-
return await sess.request(
121+
payload = json.dumps({**plain, "csrf_token": session.csrf_token})
122+
return await session.request(
145123
method,
146124
url.replace("/api/", "/weapi/"),
147-
params={"csrf_token": sess.csrf_token},
125+
params={"csrf_token": session.csrf_token},
148126
data={**WeapiEncrypt(payload)},
149127
)
150128

151129
# 来自 https://github.com/Binaryify/NeteaseCloudMusicApi
152130
@_BaseWrapper
153-
async def LapiCryptoRequest(url, plain, method):
154-
"""Linux API - 适用于Linux客户端部分APIs"""
155-
payload = {"method": method, "url": GetCurrentSession().HOST + url, "params": plain}
156-
payload = json.dumps(payload)
157-
return await GetCurrentSession().request(
158-
method,
159-
"/api/linux/forward",
160-
headers={"User-Agent": GetCurrentSession().UA_LINUX_API},
161-
data={**LinuxApiEncrypt(payload)},
162-
)
163-
164-
# 来自 https://github.com/Binaryify/NeteaseCloudMusicApi
165-
@_BaseWrapper
166-
async def EapiCryptoRequest(url, plain, method):
131+
async def EapiCryptoRequest(session, url, plain, method):
167132
"""Eapi - 适用于新版客户端绝大部分API"""
168133
payload = {**plain, "header": json.dumps({
169-
**GetCurrentSession().eapi_config,
134+
**session.eapi_config,
170135
"requestId": str(randrange(20000000,30000000))
171136
})}
172137
digest = EapiEncrypt(urllib.parse.urlparse(url).path.replace("/eapi/", "/api/"), json.dumps(payload))
173-
request = await GetCurrentSession().request(
138+
request = await session.request(
174139
method,
175140
url,
176-
headers={"User-Agent": GetCurrentSession().UA_EAPI, "Referer": ''},
141+
headers={"User-Agent": session.UA_EAPI, "Referer": ''},
177142
cookies={
178-
**GetCurrentSession().eapi_config
143+
**session.eapi_config
179144
},
180145
data={
181146
**digest

pyncm_async/apis/cloud.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# -*- coding: utf-8 -*-
22
"""我的音乐云盘 - Cloud APIs"""
33
import json
4-
from . import WeapiCryptoRequest, LoginRequiredApi, EapiCryptoRequest, GetCurrentSession
4+
from . import WeapiCryptoRequest, EapiCryptoRequest, GetCurrentSession
55

66
BUCKET = "jd-musicrep-privatecloud-audio-public"
77

88

99
@WeapiCryptoRequest
10-
@LoginRequiredApi
1110
def GetCloudDriveInfo(limit=30, offset=0):
1211
"""PC端 - 获取个人云盘内容
1312
@@ -22,7 +21,6 @@ def GetCloudDriveInfo(limit=30, offset=0):
2221

2322

2423
@WeapiCryptoRequest
25-
@LoginRequiredApi
2624
def GetCloudDriveItemInfo(song_ids: list):
2725
"""PC端 - 获取个人云盘项目详情
2826
@@ -181,8 +179,7 @@ def SetPublishCloudResource(songid):
181179
}
182180

183181

184-
@LoginRequiredApi
185-
def SetRectifySongId(oldSongId, newSongId):
182+
def SetRectifySongId(oldSongId, newSongId,session=None):
186183
"""移动端 - 歌曲纠偏
187184
188185
Args:
@@ -193,7 +190,7 @@ def SetRectifySongId(oldSongId, newSongId):
193190
dict
194191
"""
195192
return (
196-
GetCurrentSession()
193+
(session or GetCurrentSession())
197194
.get(
198195
"/api/cloud/user/song/match",
199196
params={"songId": str(oldSongId), "adjustSongId": str(newSongId)},

pyncm_async/apis/login.py

+21-60
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,21 @@
1313
import time
1414

1515

16-
def WriteLoginInfo(response):
17-
"""写登录态入当前 Session
16+
def WriteLoginInfo(response, session):
17+
"""写登录态入Session
1818
1919
Args:
2020
response (dict): 解码后的登录态
2121
2222
Raises:
2323
LoginFailedException: 登陆失败时发生
2424
"""
25-
sess = GetCurrentSession()
26-
sess.login_info = {"tick": time.time(), "content": response}
27-
if not sess.login_info["content"]["code"] == 200:
28-
sess.login_info["success"] = False
29-
raise LoginFailedException(sess.login_info["content"])
30-
sess.login_info["success"] = True
31-
sess.csrf_token = sess.cookies.get('__csrf')
25+
session.login_info = {"tick": time.time(), "content": response}
26+
if not session.login_info["content"]["code"] == 200:
27+
session.login_info["success"] = False
28+
raise LoginFailedException(session.login_info["content"])
29+
session.login_info["success"] = True
30+
session.csrf_token = session.cookies.get('__csrf')
3231

3332

3433
@WeapiCryptoRequest
@@ -103,7 +102,7 @@ def GetCurrentLoginStatus():
103102
return "/weapi/w/nuser/account/get", {}
104103

105104

106-
async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ctcode=86, remeberLogin=True) -> dict:
105+
async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ctcode=86, remeberLogin=True, session=None) -> dict:
107106
"""PC 端 - 手机号登陆
108107
109108
* 若同时指定 password 和 passwordHash, 优先使用 password
@@ -125,7 +124,7 @@ async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ct
125124
dict
126125
"""
127126
path = "/eapi/w/login/cellphone"
128-
sess = GetCurrentSession()
127+
session = session or GetCurrentSession()
129128
if password:
130129
passwordHash = HashHexDigest(password)
131130

@@ -148,11 +147,11 @@ async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ct
148147
)
149148
)()
150149

151-
WriteLoginInfo(login_status)
152-
return {'code':200,'result':sess.login_info}
150+
WriteLoginInfo(login_status, session)
151+
return {'code':200,'result':session.login_info}
153152

154153

155-
async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True) -> dict:
154+
async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True, session=None) -> dict:
156155
"""网页端 - 邮箱登陆
157156
158157
* 若同时指定 password 和 passwordHash, 优先使用 password
@@ -171,7 +170,7 @@ async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True
171170
dict
172171
"""
173172
path = "/eapi/login"
174-
sess = GetCurrentSession()
173+
session = session or GetCurrentSession()
175174
if password:
176175
passwordHash = HashHexDigest(password)
177176

@@ -192,10 +191,10 @@ async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True
192191
)
193192
)()
194193

195-
WriteLoginInfo(login_status)
196-
return {'code':200,'result':sess.login_info}
194+
WriteLoginInfo(login_status,session)
195+
return {'code':200,'result':session.login_info}
197196

198-
async def LoginViaAnonymousAccount(deviceId=None):
197+
async def LoginViaAnonymousAccount(deviceId=None, session=None):
199198
'''PC 端 - 游客登陆
200199
201200
Args:
@@ -207,8 +206,9 @@ async def LoginViaAnonymousAccount(deviceId=None):
207206
Returns:
208207
dict
209208
'''
209+
session = session or GetCurrentSession()
210210
if not deviceId:
211-
deviceId = GetCurrentSession().deviceId
211+
deviceId = session.deviceId
212212
login_status = WeapiCryptoRequest(
213213
lambda: ("/api/register/anonimous" , {
214214
"username" : b64encode(
@@ -230,8 +230,8 @@ async def LoginViaAnonymousAccount(deviceId=None):
230230
'id' : login_status['userId'],
231231
**login_status
232232
}
233-
})
234-
return GetCurrentSession().login_info
233+
}, session)
234+
return session.login_info
235235

236236
@WeapiCryptoRequest
237237
def SetSendRegisterVerifcationCodeViaCellphone(cell: str, ctcode=86):
@@ -294,45 +294,6 @@ def SetRegisterAccountViaCellphone(
294294
"phone": str(cell),
295295
}
296296

297-
async def LoginViaAnonymousAccount(deviceId=None):
298-
'''PC 端 - 游客登陆
299-
300-
Args:
301-
deviceId (str optional): 设备 ID. 设置非 None 将同时改变 Session 的设备 ID. Defaults to None.
302-
303-
Notes:
304-
Session 默认使用 `pyncm!` 作为设备 ID
305-
306-
Returns:
307-
dict
308-
'''
309-
if deviceId:
310-
GetCurrentSession().deviceId = deviceId
311-
deviceId = GetCurrentSession().deviceId
312-
login_status = await WeapiCryptoRequest(
313-
lambda: ("/api/register/anonimous" , {
314-
"username" : b64encode(
315-
('%s %s' % (
316-
deviceId,
317-
cloudmusic_dll_encode_id(deviceId))).encode()
318-
).decode()
319-
}
320-
)
321-
)()
322-
assert login_status['code'] == 200,"匿名登陆失败"
323-
WriteLoginInfo({
324-
**login_status,
325-
'profile':{
326-
'nickname' : 'Anonymous',
327-
**login_status
328-
},
329-
'account':{
330-
'id' : login_status['userId'],
331-
**login_status
332-
}
333-
})
334-
return GetCurrentSession().login_info
335-
336297
@EapiCryptoRequest
337298
def CheckIsCellphoneRegistered(cell: str, prefix=86):
338299
"""移动端 - 检查某手机号是否已注册

pyncm_async/apis/miniprograms/radio.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# -*- coding: utf-8 -*-
22
"""私人FM - Raido APIs"""
33

4-
from .. import EapiCryptoRequest, LoginRequiredApi
4+
from .. import EapiCryptoRequest
55

66

77
@EapiCryptoRequest
8-
@LoginRequiredApi
98
def GetMoreRaidoContent(limit=3, e_r=True):
109
"""PC 端 - 拉取更多FM内容
1110
@@ -20,7 +19,6 @@ def GetMoreRaidoContent(limit=3, e_r=True):
2019

2120

2221
@EapiCryptoRequest
23-
@LoginRequiredApi
2422
def SetSkipRadioContent(songId, time=0, alg="itembased", e_r=True):
2523
"""PC 端 - 跳过 FM 歌曲
2624
@@ -42,7 +40,6 @@ def SetSkipRadioContent(songId, time=0, alg="itembased", e_r=True):
4240

4341

4442
@EapiCryptoRequest
45-
@LoginRequiredApi
4643
def SetLikeRadioContent(trackId, like=True, time="0", alg="itembased", e_r=True):
4744
"""PC 端 - `收藏喜欢` FM 歌曲
4845
@@ -66,7 +63,6 @@ def SetLikeRadioContent(trackId, like=True, time="0", alg="itembased", e_r=True)
6663

6764

6865
@EapiCryptoRequest
69-
@LoginRequiredApi
7066
def SetTrashRadioContent(songId, time="0", alg="itembased", e_r=True):
7167
"""PC 端 - 删除 FM 歌曲
7268

0 commit comments

Comments
 (0)