Skip to content

Commit ff6d50f

Browse files
committed
feat: enhance lxns provider
1 parent 32ace5c commit ff6d50f

File tree

4 files changed

+46
-29
lines changed

4 files changed

+46
-29
lines changed

docs/changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# 更新日志
22

3+
## 1.3.6 (2025-08-22)
4+
5+
Features:
6+
- WechatProvider 现支持 获取好友 和 设置对手 的功能
7+
- WechatProvider 现支持 IPlayerProvider 接口 -> [#36](https://github.com/TrueRou/maimai.py/pull/36)
8+
- LXNSProvider 现支持 使用OAuth认证的个人API密钥 来访问接口
9+
10+
Bugfixes:
11+
- 修复 LXNSProvider 在获取单个成绩、最佳成绩时无法使用个人API密钥
12+
313
## 1.3.5 (2025-08-05)
414

515
Features:

docs/providers/lxns.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
- 使用好友代码:`PlayerIdentifier(friend_code="123456789")`,将使用开发者API。
1515
- 使用QQ号:`PlayerIdentifier(qq="123456789")`,将使用开发者API。
1616

17-
::: warning
18-
注意:获取单个成绩(`maimai.minfo(...)`)时,目前仅支持开发者API。
19-
:::
20-
2117
## 关于开发者API
2218

2319
申请开发者Token:https://maimai.lxns.net/developer

docs/providers/wechat.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
舞萌服务号的舞萌DX页面,通过HTML解析获取信息。
44

5-
实现:IScoreProvider, IPlayerIdentifierProvider
5+
实现:IScoreProvider, IPlayerIdentifierProvider, IPlayerProvider
66

77
源站:https://maimai.wahlap.com/maimai-mobile/
88

@@ -22,6 +22,19 @@
2222
参考 [proxy_updater (示例项目)](../samples/proxy_updater.md) 部分,这是一个通过代理和微信 OAuth 认证更新查分器的示例。
2323
:::
2424

25+
## 关于 Rival (对手)
26+
27+
通过 NET 的隐藏接口,您可以修改到玩家的对手信息。
28+
29+
```python
30+
wechat = WechatProvider()
31+
identifier = await maimai.wechat(r="r", t="t", code="code", state="state")
32+
friends = await wechat.get_friends(identifier, maimai) # 需要主动传入 MaimaiClient 实例
33+
await wechat.set_rival_on(identifier, friends[0], maimai) # 设置第一个好友为对手
34+
```
35+
36+
鉴于接口原因,我们无法查询玩家已有的对手信息,只能开启和关闭对手。
37+
2538
## 已知问题
2639

2740
- WechatProvider 可能出现轻微内存泄露问题。

maimai_py/providers/lxns.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import dataclasses
33
import hashlib
4+
import re
45
from functools import reduce
56
from json import JSONDecodeError
67
from operator import concat
@@ -17,6 +18,8 @@
1718
if TYPE_CHECKING:
1819
from maimai_py.maimai import MaimaiClient, MaimaiSongs
1920

21+
is_jwt = re.compile(r"^[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$")
22+
2023

2124
class LXNSProvider(ISongProvider, IPlayerProvider, IScoreProvider, IScoreUpdateProvider, IAliasProvider, IItemListProvider):
2225
"""The provider that fetches data from the LXNS.
@@ -61,7 +64,10 @@ async def _build_player_request(self, path: str, identifier: PlayerIdentifier, c
6164
# user-level API takes the precedence: If personal token provided, use it first
6265
assert isinstance(identifier.credentials, str)
6366
entrypoint = f"api/v0/user/maimai/player/{path}"
64-
headers = {"X-User-Token": identifier.credentials}
67+
if is_jwt.match(identifier.credentials) is not None:
68+
headers = {"Authorization": f"Bearer {identifier.credentials}"}
69+
else:
70+
headers = {"X-User-Token": identifier.credentials}
6571
else:
6672
await self._ensure_friend_code(client, identifier)
6773
entrypoint = f"api/v0/maimai/player/{identifier.friend_code}/{path}"
@@ -235,44 +241,36 @@ async def get_scores_all(self, identifier: PlayerIdentifier, client: "MaimaiClie
235241

236242
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(RequestError), reraise=True)
237243
async def get_scores_best(self, identifier: PlayerIdentifier, client: "MaimaiClient") -> list[Score]:
238-
if identifier.friend_code is None:
239-
return await self.get_scores_all(identifier, client)
240-
await self._ensure_friend_code(client, identifier)
241-
entrypoint = f"api/v0/maimai/player/{identifier.friend_code}/bests"
242-
resp = await client._client.get(self.base_url + entrypoint, headers=self.headers)
244+
url, headers, _ = await self._build_player_request("bests", identifier, client)
245+
resp = await client._client.get(url, headers=headers)
243246
resp_data = self._check_response_player(resp)["data"]
244247
return [s for score in resp_data["standard"] + resp_data["dx"] if (s := LXNSProvider._deser_score(score))]
245248

246249
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(RequestError), reraise=True)
247250
async def get_scores_one(self, identifier: PlayerIdentifier, song: Song, client: "MaimaiClient") -> list[Score]:
248-
await self._ensure_friend_code(client, identifier)
249-
request_tasks, create_task = [], lambda type: asyncio.create_task(
250-
client._client.get(
251-
self.base_url + f"api/v0/maimai/player/{identifier.friend_code}/bests",
252-
params={"song_id": song.id if type != SongType.UTAGE else song.id + 100000, "song_type": type.value},
253-
headers=self.headers,
254-
)
255-
)
251+
async def get_scores_for_type(type: SongType) -> list[Score]:
252+
url, headers, _ = await self._build_player_request("bests", identifier, client)
253+
params = {"song_id": song.id if type != SongType.UTAGE else song.id + 100000, "song_type": type.value}
254+
resp = await client._client.get(url, params=params, headers=headers)
255+
resp_data = self._check_response_player(resp)["data"]
256+
return [s for score in resp_data if (s := LXNSProvider._deser_score(score))]
257+
258+
results, _ = [], await self._ensure_friend_code(client, identifier)
256259
if len(song.difficulties.standard) > 0:
257-
request_tasks.append(create_task(SongType.STANDARD))
260+
results.append(asyncio.create_task(get_scores_for_type(SongType.STANDARD)))
258261
if len(song.difficulties.dx) > 0:
259-
request_tasks.append(create_task(SongType.DX))
262+
results.append(asyncio.create_task(get_scores_for_type(SongType.DX)))
260263
if len(song.difficulties.utage) > 0:
261-
request_tasks.append(create_task(SongType.UTAGE))
262-
resps = await asyncio.gather(*request_tasks)
263-
resp_data = [self._check_response_player(resp)["data"] for resp in resps]
264-
return [s for score in reduce(concat, resp_data) if (s := LXNSProvider._deser_score(score))]
264+
results.append(asyncio.create_task(get_scores_for_type(SongType.UTAGE)))
265+
return [score for score in reduce(concat, await asyncio.gather(*results))]
265266

266267
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(RequestError), reraise=True)
267268
async def update_scores(self, identifier: PlayerIdentifier, scores: Iterable[Score], client: "MaimaiClient") -> None:
268269
maimai_songs = await client.songs()
269270
url, headers, _ = await self._build_player_request("scores", identifier, client)
270271
scores_dict = {"scores": [json for score in scores if (json := await LXNSProvider._ser_score(score, maimai_songs))]}
271272
resp = await client._client.post(url, headers=headers, json=scores_dict)
272-
resp.raise_for_status()
273-
resp_json = resp.json()
274-
if not resp_json["success"] and resp_json["code"] == 400:
275-
raise ValueError(resp_json["message"])
273+
self._check_response_player(resp)
276274

277275
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(RequestError), reraise=True)
278276
async def get_aliases(self, client: "MaimaiClient") -> dict[int, list[str]]:

0 commit comments

Comments
 (0)