Skip to content

Commit

Permalink
Version 0.1.0
Browse files Browse the repository at this point in the history
api: API `session=` 关键字支持 (#55)
  • Loading branch information
mos9527 committed Feb 22, 2024
1 parent 654c922 commit b5ecef3
Show file tree
Hide file tree
Showing 10 changed files with 61 additions and 153 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,16 @@ async with CreateNewSession(): # 建立新的 Session,并进入该 Session,
# 离开 Session. 此后 API 将继续由全局 Session 管理
await GetTrackComments(...)
```
使用 `with`...
- 注:Session 各*线程*独立,各线程利用 `with` 设置的 Session 不互相影响
- 注:Session 离开 `with` clause 时,**Session 会被销毁**,但不会影响全局 Session
- 注:Session 生命周期细节请参阅 https://www.python-httpx.org/async/

同时,你也可以在 API Call 中 指定 Session
```python
await GetTrackComments(..., session=session)
```

详见 [Session 说明](https://github.com/mos9527/pyncm/blob/async/pyncm/__init__.py#L35)
## API 说明
大部分 API 函数已经详细注释,可读性较高。推荐参阅 [API 源码](https://github.com/mos9527/pyncm/tree/async/pyncm) 获得支持
Expand Down
3 changes: 2 additions & 1 deletion pyncm-async.tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import asyncio
import logging
import pyncm_async, pyncm_async.apis
logging.basicConfig(level=0)
logging.basicConfig(level=logging.WARNING)
logging.getLogger('pyncm.api').setLevel(logging.DEBUG)
# Account from https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/test/login.test.js
pyncm_async.SetCurrentSession(
pyncm_async.LoadSessionFromString(
Expand Down
4 changes: 2 additions & 2 deletions pyncm_async/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""PyNCM-Async 网易云音乐 Python 异步 API / 下载工具"""
__VERSION_MAJOR__ = 0
__VERSION_MINOR__ = 0
__VERSION_PATCH__ = 3
__VERSION_MINOR__ = 1
__VERSION_PATCH__ = 0

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

Expand Down
75 changes: 20 additions & 55 deletions pyncm_async/apis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,32 @@ def _BaseWrapper(requestFunc):
实际使用请参考以下其他 Wrapper::
LoginRequiredApi
UserIDBasedApi
WeapiCryptoRequest
LapiCryptoRequest
EapiCryptoRequest
"""
@wraps(requestFunc)
def apiWrapper(apiFunc):
@wraps(apiFunc)
async def wrapper(*a, **k):
# HACK: 'session=' keyword support
session = k.get("session", GetCurrentSession())
# HACK: For now,wrapped functions will not have access to the session object
if 'session' in k: del k['session']

ret = apiFunc(*a, **k)
url, payload = ret[:2]
method = ret[-1] if ret[-1] in ["POST", "GET"] else "POST"
logger.debug('TYPE=%s API=%s.%s %s url=%s deviceId=%s payload=%s' % (
logger.debug('TYPE=%s API=%s.%s %s url=%s deviceId=%s payload=%s session=0x%x' % (
requestFunc.__name__.split('Crypto')[0].upper(),
apiFunc.__module__,
apiFunc,
apiFunc.__name__,
method,
url,
GetCurrentSession().deviceId,
payload)
session.deviceId,
payload,
id(session))
)
rsp = await requestFunc(url, payload, method)
rsp = await requestFunc(session, url, payload, method)
try:
payload = rsp.text if isinstance(rsp, Response) else rsp
payload = payload.decode() if not isinstance(payload, str) else payload
Expand All @@ -100,30 +103,6 @@ async def wrapper(*a, **k):

return apiWrapper


def LoginRequiredApi(func):
"""API 需要事先登录"""
@wraps(func)
def wrapper(*a, **k):
if not GetCurrentSession().login_info["success"]:
raise LOGIN_REQUIRED
return func(*a, **k)

return wrapper


def UserIDBasedApi(func):
"""API 第一参数为用户 ID,而该参数可留 0 而指代已登录的用户 ID"""
@wraps(func)
def wrapper(user_id=0, *a, **k):
if user_id == 0 and GetCurrentSession().login_info["success"]:
user_id = GetCurrentSession().uid
elif user_id == 0:
raise LOGIN_REQUIRED
return func(user_id, *a, **k)

return wrapper

def EapiEncipered(func):
"""函数值有 Eapi 加密 - 解密并返回原文"""
@wraps(func)
Expand All @@ -137,45 +116,31 @@ def wrapper(*a, **k):
return wrapper

@_BaseWrapper
async def WeapiCryptoRequest(url, plain, method):
async def WeapiCryptoRequest(session, url, plain, method):
"""Weapi - 适用于 网页端、小程序、手机端部分 APIs"""
sess = GetCurrentSession()
payload = json.dumps({**plain, "csrf_token": sess.csrf_token})
return await sess.request(
payload = json.dumps({**plain, "csrf_token": session.csrf_token})
return await session.request(
method,
url.replace("/api/", "/weapi/"),
params={"csrf_token": sess.csrf_token},
params={"csrf_token": session.csrf_token},
data={**WeapiEncrypt(payload)},
)

# 来自 https://github.com/Binaryify/NeteaseCloudMusicApi
@_BaseWrapper
async def LapiCryptoRequest(url, plain, method):
"""Linux API - 适用于Linux客户端部分APIs"""
payload = {"method": method, "url": GetCurrentSession().HOST + url, "params": plain}
payload = json.dumps(payload)
return await GetCurrentSession().request(
method,
"/api/linux/forward",
headers={"User-Agent": GetCurrentSession().UA_LINUX_API},
data={**LinuxApiEncrypt(payload)},
)

# 来自 https://github.com/Binaryify/NeteaseCloudMusicApi
@_BaseWrapper
async def EapiCryptoRequest(url, plain, method):
async def EapiCryptoRequest(session, url, plain, method):
"""Eapi - 适用于新版客户端绝大部分API"""
payload = {**plain, "header": json.dumps({
**GetCurrentSession().eapi_config,
**session.eapi_config,
"requestId": str(randrange(20000000,30000000))
})}
digest = EapiEncrypt(urllib.parse.urlparse(url).path.replace("/eapi/", "/api/"), json.dumps(payload))
request = await GetCurrentSession().request(
request = await session.request(
method,
url,
headers={"User-Agent": GetCurrentSession().UA_EAPI, "Referer": ''},
headers={"User-Agent": session.UA_EAPI, "Referer": ''},
cookies={
**GetCurrentSession().eapi_config
**session.eapi_config
},
data={
**digest
Expand Down
9 changes: 3 additions & 6 deletions pyncm_async/apis/cloud.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
"""我的音乐云盘 - Cloud APIs"""
import json
from . import WeapiCryptoRequest, LoginRequiredApi, EapiCryptoRequest, GetCurrentSession
from . import WeapiCryptoRequest, EapiCryptoRequest, GetCurrentSession

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


@WeapiCryptoRequest
@LoginRequiredApi
def GetCloudDriveInfo(limit=30, offset=0):
"""PC端 - 获取个人云盘内容
Expand All @@ -22,7 +21,6 @@ def GetCloudDriveInfo(limit=30, offset=0):


@WeapiCryptoRequest
@LoginRequiredApi
def GetCloudDriveItemInfo(song_ids: list):
"""PC端 - 获取个人云盘项目详情
Expand Down Expand Up @@ -181,8 +179,7 @@ def SetPublishCloudResource(songid):
}


@LoginRequiredApi
def SetRectifySongId(oldSongId, newSongId):
def SetRectifySongId(oldSongId, newSongId,session=None):
"""移动端 - 歌曲纠偏
Args:
Expand All @@ -193,7 +190,7 @@ def SetRectifySongId(oldSongId, newSongId):
dict
"""
return (
GetCurrentSession()
(session or GetCurrentSession())
.get(
"/api/cloud/user/song/match",
params={"songId": str(oldSongId), "adjustSongId": str(newSongId)},
Expand Down
81 changes: 21 additions & 60 deletions pyncm_async/apis/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,21 @@
import time


def WriteLoginInfo(response):
"""写登录态入当前 Session
def WriteLoginInfo(response, session):
"""写登录态入Session
Args:
response (dict): 解码后的登录态
Raises:
LoginFailedException: 登陆失败时发生
"""
sess = GetCurrentSession()
sess.login_info = {"tick": time.time(), "content": response}
if not sess.login_info["content"]["code"] == 200:
sess.login_info["success"] = False
raise LoginFailedException(sess.login_info["content"])
sess.login_info["success"] = True
sess.csrf_token = sess.cookies.get('__csrf')
session.login_info = {"tick": time.time(), "content": response}
if not session.login_info["content"]["code"] == 200:
session.login_info["success"] = False
raise LoginFailedException(session.login_info["content"])
session.login_info["success"] = True
session.csrf_token = session.cookies.get('__csrf')


@WeapiCryptoRequest
Expand Down Expand Up @@ -103,7 +102,7 @@ def GetCurrentLoginStatus():
return "/weapi/w/nuser/account/get", {}


async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ctcode=86, remeberLogin=True) -> dict:
async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ctcode=86, remeberLogin=True, session=None) -> dict:
"""PC 端 - 手机号登陆
* 若同时指定 password 和 passwordHash, 优先使用 password
Expand All @@ -125,7 +124,7 @@ async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ct
dict
"""
path = "/eapi/w/login/cellphone"
sess = GetCurrentSession()
session = session or GetCurrentSession()
if password:
passwordHash = HashHexDigest(password)

Expand All @@ -148,11 +147,11 @@ async def LoginViaCellphone(phone="", password="",passwordHash="",captcha="", ct
)
)()

WriteLoginInfo(login_status)
return {'code':200,'result':sess.login_info}
WriteLoginInfo(login_status, session)
return {'code':200,'result':session.login_info}


async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True) -> dict:
async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True, session=None) -> dict:
"""网页端 - 邮箱登陆
* 若同时指定 password 和 passwordHash, 优先使用 password
Expand All @@ -171,7 +170,7 @@ async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True
dict
"""
path = "/eapi/login"
sess = GetCurrentSession()
session = session or GetCurrentSession()
if password:
passwordHash = HashHexDigest(password)

Expand All @@ -192,10 +191,10 @@ async def LoginViaEmail(email="", password="",passwordHash="", remeberLogin=True
)
)()

WriteLoginInfo(login_status)
return {'code':200,'result':sess.login_info}
WriteLoginInfo(login_status,session)
return {'code':200,'result':session.login_info}

async def LoginViaAnonymousAccount(deviceId=None):
async def LoginViaAnonymousAccount(deviceId=None, session=None):
'''PC 端 - 游客登陆
Args:
Expand All @@ -207,8 +206,9 @@ async def LoginViaAnonymousAccount(deviceId=None):
Returns:
dict
'''
session = session or GetCurrentSession()
if not deviceId:
deviceId = GetCurrentSession().deviceId
deviceId = session.deviceId
login_status = WeapiCryptoRequest(
lambda: ("/api/register/anonimous" , {
"username" : b64encode(
Expand All @@ -230,8 +230,8 @@ async def LoginViaAnonymousAccount(deviceId=None):
'id' : login_status['userId'],
**login_status
}
})
return GetCurrentSession().login_info
}, session)
return session.login_info

@WeapiCryptoRequest
def SetSendRegisterVerifcationCodeViaCellphone(cell: str, ctcode=86):
Expand Down Expand Up @@ -294,45 +294,6 @@ def SetRegisterAccountViaCellphone(
"phone": str(cell),
}

async def LoginViaAnonymousAccount(deviceId=None):
'''PC 端 - 游客登陆
Args:
deviceId (str optional): 设备 ID. 设置非 None 将同时改变 Session 的设备 ID. Defaults to None.
Notes:
Session 默认使用 `pyncm!` 作为设备 ID
Returns:
dict
'''
if deviceId:
GetCurrentSession().deviceId = deviceId
deviceId = GetCurrentSession().deviceId
login_status = await WeapiCryptoRequest(
lambda: ("/api/register/anonimous" , {
"username" : b64encode(
('%s %s' % (
deviceId,
cloudmusic_dll_encode_id(deviceId))).encode()
).decode()
}
)
)()
assert login_status['code'] == 200,"匿名登陆失败"
WriteLoginInfo({
**login_status,
'profile':{
'nickname' : 'Anonymous',
**login_status
},
'account':{
'id' : login_status['userId'],
**login_status
}
})
return GetCurrentSession().login_info

@EapiCryptoRequest
def CheckIsCellphoneRegistered(cell: str, prefix=86):
"""移动端 - 检查某手机号是否已注册
Expand Down
6 changes: 1 addition & 5 deletions pyncm_async/apis/miniprograms/radio.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
"""私人FM - Raido APIs"""

from .. import EapiCryptoRequest, LoginRequiredApi
from .. import EapiCryptoRequest


@EapiCryptoRequest
@LoginRequiredApi
def GetMoreRaidoContent(limit=3, e_r=True):
"""PC 端 - 拉取更多FM内容
Expand All @@ -20,7 +19,6 @@ def GetMoreRaidoContent(limit=3, e_r=True):


@EapiCryptoRequest
@LoginRequiredApi
def SetSkipRadioContent(songId, time=0, alg="itembased", e_r=True):
"""PC 端 - 跳过 FM 歌曲
Expand All @@ -42,7 +40,6 @@ def SetSkipRadioContent(songId, time=0, alg="itembased", e_r=True):


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


@EapiCryptoRequest
@LoginRequiredApi
def SetTrashRadioContent(songId, time="0", alg="itembased", e_r=True):
"""PC 端 - 删除 FM 歌曲
Expand Down
Loading

0 comments on commit b5ecef3

Please sign in to comment.