Compare commits

...

13 Commits

Author SHA1 Message Date
mokurin000
89e096bac6 typing: parsedResponseDict 2025-07-30 11:23:31 +08:00
mokurin000
044c23ff71 chore: generate timestamp 2025-07-30 04:29:35 +08:00
mokurin000
6d234bc2b3 fix: make 2024 api easier to use 2025-07-30 03:20:54 +08:00
mokurin000
47dcc2e045 chore: ping api call 2025-07-30 01:54:02 +08:00
mokurin000
41a2ad5ae1 chore: sdgb140 2025-07-30 01:43:30 +08:00
mokurin000
c304c24863 style: re-format special code 2025-07-29 17:08:45 +08:00
mokurin000
cb599c5f35 chore: totally fix lint issues 2025-07-29 17:07:34 +08:00
mokurin000
ae6a4d1cf4 chore: fix more warnings 2025-07-29 17:01:49 +08:00
mokurin000
9a2e8bd1ed feat:: custom uid input 2025-07-29 15:20:28 +08:00
mokurin000
12093c9b9b refactor: module imports 2025-07-29 02:27:10 +08:00
mokurin000
e99f04c416 chore: unlock ソルト 2025-07-29 02:06:19 +08:00
mokurin000
4fad3aba71 chore: remove evil code 2025-07-29 01:50:42 +08:00
mokurin000
52c9e205e5 build: migrate to uv 2025-07-29 01:43:56 +08:00
30 changed files with 1510 additions and 883 deletions

2
.gitignore vendored
View File

@@ -175,3 +175,5 @@ cython_debug/
.pypirc
Private_Static_Settings.py
Private_Static_Settings.py
/.python-version

View File

@@ -8,25 +8,36 @@ import requests
import json
import re
from loguru import logger
# 常量
CHIP_ID = "A63E-01E68606624"
COMMON_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
API_URL = "http://ai.sys-allnet.cn/wc_aime/api/get_data"
# 计算 SHA256
def getSHA256(input_str):
"""SHA256计算"""
return hashlib.sha256(input_str.encode('utf-8')).hexdigest().upper()
return hashlib.sha256(input_str.encode("utf-8")).hexdigest().upper()
# 生成时间戳
def generateSEGATimestamp():
"""SEGA格式的 YYMMDDHHMMSS 时间戳sb玩意"""
return time.strftime("%y%m%d%H%M%S", time.localtime())
# 计算认证 key
def calcSEGAAimeDBAuthKey(varString:str, timestamp:str, commonKey:str="XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW") -> str:
def calcSEGAAimeDBAuthKey(
varString: str, timestamp: str, commonKey: str = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
) -> str:
"""计算 SEGA AimeDB 的认证 key"""
return hashlib.sha256((varString + timestamp + commonKey).encode("utf-8")).hexdigest().upper()
return (
hashlib.sha256((varString + timestamp + commonKey).encode("utf-8"))
.hexdigest()
.upper()
)
def apiAimeDB(qrCode):
"""AimeDB 扫码 API 实现"""
@@ -42,11 +53,11 @@ def apiAimeDB(qrCode):
"openGameID": "MAID",
"key": currentKey,
"qrCode": qrCode,
"timestamp": timestamp
"timestamp": timestamp,
}
# 输出准备好的请求数据
print("Payload:", json.dumps(payload, separators=(',', ':')))
print("Payload:", json.dumps(payload, separators=(",", ":")))
# 发送 POST 请求
headers = {
@@ -55,7 +66,9 @@ def apiAimeDB(qrCode):
"User-Agent": "WC_AIME_LIB",
"Content-Type": "application/json",
}
response = requests.post(API_URL, data=json.dumps(payload, separators=(',', ':')), headers=headers)
response = requests.post(
API_URL, data=json.dumps(payload, separators=(",", ":")), headers=headers
)
# 返回服务器的响应
return response
@@ -64,16 +77,16 @@ def apiAimeDB(qrCode):
def isSGWCFormat(input_string: str) -> bool:
"""简单检查二维码字符串是否符合格式"""
if (
len(input_string) != 84 #长度
or not input_string.startswith("SGWCMAID") #识别字
or re.match("^[0-9A-F]+$", input_string[20:]) is None #有效字符
len(input_string) != 84 # 长度
or not input_string.startswith("SGWCMAID") # 识别字
or re.match("^[0-9A-F]+$", input_string[20:]) is None # 有效字符
):
return False
else:
return True
def implAimeDB(qrCode:str, isAlreadyFinal:bool=False) -> str:
def implAimeDB(qrCode: str, isAlreadyFinal: bool = False) -> str:
"""
Aime DB 的请求的参考实现。
提供完整 QRCode 内容返回响应的字符串Json格式
@@ -93,7 +106,7 @@ def implAimeDB(qrCode:str, isAlreadyFinal:bool=False) -> str:
return response.text
def implGetUID(qr_content:str) -> dict:
def implGetUID(qr_content: str) -> dict:
"""
包装后的 UID 扫码器实现。
此函数会返回 AimeDB 传回的 Json 转成 Python 字典的结果。
@@ -101,18 +114,19 @@ def implGetUID(qr_content:str) -> dict:
"""
# 检查格式
if not isSGWCFormat(qr_content):
return {'errorID': 60001} # 二维码内容明显无效
return {"errorID": 60001} # 二维码内容明显无效
# 发送请求并处理响应
try:
result = json.loads(implAimeDB(qr_content))
logger.info(f"QRScan Got Response {result}")
except:
return {'errorID': 60002} # 无法解码 Response 的内容
except Exception:
return {"errorID": 60002} # 无法解码 Response 的内容
# 返回结果
return result
if __name__ == "__main__":
userInputQR = input("QRCode: ")
print(implAimeDB(userInputQR))

View File

@@ -7,120 +7,132 @@ from loguru import logger
from urllib.parse import parse_qs
import configparser as ini
LITE_AUTH_KEY = bytes([47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71])
LITE_AUTH_IV = bytes.fromhex('00000000000000000000000000000000')
LITE_AUTH_KEY = bytes(
[47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71]
)
LITE_AUTH_IV = bytes.fromhex("00000000000000000000000000000000")
def auth_lite_encrypt(plaintext: str) -> bytes:
# 构造数据16字节头 + 16字节0前缀 + 明文
header = bytes(16)
content = bytes(16) + plaintext.encode('utf-8')
content = bytes(16) + plaintext.encode("utf-8")
data = header + content
# 填充并加密
padded_data = pad(data, AES.block_size)
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
return cipher.encrypt(padded_data)
def auth_lite_decrypt(ciphertext: bytes) -> str:
# 解密并去除填充
cipher = AES.new(LITE_AUTH_KEY, AES.MODE_CBC, LITE_AUTH_IV)
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
# 提取内容并解码
content = decrypted_data[16:] # 去除头部的16字节
return content.decode('utf-8').strip()
return content.decode("utf-8").strip()
def getRawDelivery(title_ver:str="1.51"):
encrypted = auth_lite_encrypt(f'title_id=SDGB&title_ver={title_ver}&client_id=A63E01C2805')
def getRawDelivery(title_ver: str = "1.51"):
encrypted = auth_lite_encrypt(
f"title_id=SDGB&title_ver={title_ver}&client_id=A63E01C2805"
)
r = httpx.post(
'http://at.sys-allnet.cn/net/delivery/instruction',
data = encrypted,
headers = {
'User-Agent': "SDGB;Windows/Lite",
'Pragma': 'DFI'
}
"http://at.sys-allnet.cn/net/delivery/instruction",
data=encrypted,
headers={"User-Agent": "SDGB;Windows/Lite", "Pragma": "DFI"},
)
resp_data = r.content
decrypted_str = auth_lite_decrypt(resp_data)
# 过滤所有控制字符
decrypted_str = ''.join([i for i in decrypted_str if 31 < ord(i) < 127])
decrypted_str = "".join([i for i in decrypted_str if 31 < ord(i) < 127])
logger.info(f"RAW Response: {decrypted_str}")
return decrypted_str
def parseRawDelivery(deliveryStr):
"""解析 RAW 的 Delivery 字符串,返回其中的有效的 instruction URL 的列表"""
parsedResponseDict = {key: value[0] for key, value in parse_qs(deliveryStr).items()}
urlList = parsedResponseDict['uri'].split('|')
parsedResponseDict: dict[str, str] = {
key: value[0] for key, value in parse_qs(deliveryStr).items()
}
urlList = parsedResponseDict["uri"].split("|")
# 过滤掉空字符串和内容为 null 的情况
urlList = [url for url in urlList if url and url != 'null']
urlList = [url for url in urlList if url and url != "null"]
logger.info(f"Parsed URL List: {urlList}")
validURLs = []
for url in urlList:
# 检查是否是 HTTPS 的 URL以及是否是 txt 文件,否则忽略
if not url.startswith('https://') or not url.endswith('.txt'):
if not url.startswith("https://") or not url.endswith(".txt"):
logger.warning(f"Invalid URL will be ignored: {url}")
continue
validURLs.append(url)
logger.info(f"Verified Valid URLs: {validURLs}")
return validURLs
def getUpdateIniFromURL(url):
# 发送请求
response = httpx.get(url, headers={
'User-Agent': 'SDGB;Windows/Lite',
'Pragma': 'DFI'
})
response = httpx.get(
url, headers={"User-Agent": "SDGB;Windows/Lite", "Pragma": "DFI"}
)
logger.info(f"成功自 {url} 获取更新信息")
return response.text
def parseUpdateIni(iniText):
# 解析配置
config = ini.ConfigParser(allow_no_value=True)
config.read_string(iniText)
logger.info(f"成功解析配置文件,包含的节有:{config.sections()}")
# 获取 COMMON 节的配置
common = config['COMMON']
common = config["COMMON"]
# 初始化消息列表
message = []
# 获取游戏描述并去除引号
game_desc = common['GAME_DESC'].strip('"')
game_desc = common["GAME_DESC"].strip('"')
# 根据前缀选择消息模板和图标
prefix_icons = {
'PATCH': ('💾 游戏程序更新 (.app) ', 'PATCH_'),
'OPTION': ('📚 游戏内容更新 (.opt) ', 'OPTION_')
"PATCH": ("💾 游戏程序更新 (.app) ", "PATCH_"),
"OPTION": ("📚 游戏内容更新 (.opt) ", "OPTION_"),
}
icon, prefix = prefix_icons.get(game_desc.split('_')[0], ('📦 游戏更新 ', ''))
icon, prefix = prefix_icons.get(game_desc.split("_")[0], ("📦 游戏更新 ", ""))
# 构建消息标题
game_title = game_desc.replace(prefix, '', 1)
game_title = game_desc.replace(prefix, "", 1)
message.append(f"{icon}{game_title}")
# 添加可选文件的下载链接(如果有)
if 'OPTIONAL' in config:
if "OPTIONAL" in config:
message.append("往期更新包:")
optional_files = [f"{url.split('/')[-1]} {url}" for _, url in config.items('OPTIONAL')]
optional_files = [
f"{url.split('/')[-1]} {url}" for _, url in config.items("OPTIONAL")
]
message.extend(optional_files)
# 添加主文件的下载链接
main_file = common['INSTALL1']
main_file_name = main_file.split('/')[-1]
main_file = common["INSTALL1"]
main_file_name = main_file.split("/")[-1]
message.append(f"此次更新包: \n{main_file_name} {main_file}")
# 添加发布时间信息
release_time = common['RELEASE_TIME'].replace('T', ' ')
release_time = common["RELEASE_TIME"].replace("T", " ")
message.append(f"正式发布时间:{release_time}\n")
# 构建最终的消息字符串
final_message = '\n'.join(message)
final_message = "\n".join(message)
logger.info(f"消息构建完成,最终的消息为:\n{final_message}")
return final_message
if __name__ == '__main__':
urlList = parseRawDelivery(getRawDelivery("1.51"))
if __name__ == "__main__":
raw = getRawDelivery("1.51")
urlList = parseRawDelivery(raw)
for url in urlList:
iniText = getUpdateIniFromURL(url)
message = parseUpdateIni(iniText)

View File

@@ -1,43 +1,51 @@
# 舞萌DX
# 标题服务器通讯实现
import zlib
import hashlib
import httpx
from loguru import logger
import random
import time
import httpx
from loguru import logger
from ctypes import c_int32
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Config import *
from typing import Optional
import certifi
use2024Api = False # 是否使用 2024 API
from Config import (
useProxy,
proxyUrl,
)
AesKey = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"
AesIV = "d6xHIKq]1J]Dt^ue"
ObfuscateParam = "B44df8yT"
def use2024Api():
global AesKey, AesIV, ObfuscateParam
if use2024Api:
AesKey = "n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@"
AesIV = ";;KjR1C3hgB1ovXa"
ObfuscateParam = "BEs2D5vW"
else:
AesKey = "a>32bVP7v<63BVLkY[xM>daZ1s9MBP<R"
AesIV = "d6xHIKq]1J]Dt^ue"
ObfuscateParam = "B44df8yT"
class SDGBApiError(Exception):
pass
class SDGBRequestError(SDGBApiError):
pass
class SDGBResponseError(SDGBApiError):
pass
class aes_pkcs7(object):
def __init__(self, key: str, iv: str):
self.key = key.encode('utf-8')
self.iv = iv.encode('utf-8')
self.key = key.encode("utf-8")
self.iv = iv.encode("utf-8")
self.mode = AES.MODE_CBC
def encrypt(self, content: bytes) -> bytes:
@@ -55,21 +63,23 @@ class aes_pkcs7(object):
def pkcs7unpadding(self, text):
length = len(text)
unpadding = ord(text[length - 1])
return text[0:length - unpadding]
return text[0 : length - unpadding]
def pkcs7padding(self, text):
bs = 16
length = len(text)
bytes_length = len(text.encode('utf-8'))
bytes_length = len(text.encode("utf-8"))
padding_size = length if (bytes_length == length) else bytes_length
padding = bs - padding_size % bs
padding_text = chr(padding) * padding
return text + padding_text
def getSDGBApiHash(api):
# API 的 Hash 的生成
# 有空做一下 Hash 的彩虹表?
return hashlib.md5((api+"MaimaiChn"+ObfuscateParam).encode()).hexdigest()
return hashlib.md5((api + "MaimaiChn" + ObfuscateParam).encode()).hexdigest()
def apiSDGB(
data: str,
@@ -94,7 +104,7 @@ def apiSDGB(
endpoint = "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/"
# 准备好请求数据
requestDataFinal = aes.encrypt(zlib.compress(data.encode('utf-8')))
requestDataFinal = aes.encrypt(zlib.compress(data.encode("utf-8")))
if not noLog:
logger.debug(f"[Stage 1] 准备开始请求 {targetApi},以 {data}")
@@ -108,21 +118,25 @@ def apiSDGB(
httpClient = httpx.Client(proxy=proxyUrl, verify=False)
else:
httpClient = httpx.Client(verify=False)
api_hash = getSDGBApiHash(targetApi)
logger.info(f"hash: {api_hash}")
# 发送请求
response = httpClient.post(
url=endpoint + getSDGBApiHash(targetApi),
url=endpoint + api_hash,
headers={
"User-Agent": f"{getSDGBApiHash(targetApi)}#{agentExtra}",
"User-Agent": f"{api_hash}#{agentExtra}",
"Content-Type": "application/json",
"Mai-Encoding": "1.50",
"Accept-Encoding": "",
"Charset": "UTF-8",
"Content-Encoding": "deflate",
"Expect": "100-continue"
"Expect": "100-continue",
},
content=requestDataFinal, #数据
timeout=timeout
content=requestDataFinal, # 数据
timeout=timeout,
)
if not noLog:
@@ -142,30 +156,42 @@ def apiSDGB(
if not noLog:
logger.debug("[Stage 3] Decryption SUCCESS.")
except Exception as e:
logger.warning(f"[Stage 3] Decryption FAILED. Raw Content: {responseContentRaw}, Error: {e}")
logger.warning(
f"[Stage 3] Decryption FAILED. Raw Content: {responseContentRaw}, Error: {e}"
)
raise SDGBResponseError("Decryption failed")
# 然后尝试解压
try:
# 看看文件头是否是压缩过的
if responseContentDecrypted.startswith(b'\x78\x9c'):
if responseContentDecrypted.startswith(b"\x78\x9c"):
logger.debug("[Stage 4] Zlib detected, decompressing...")
responseContentFinal = zlib.decompress(responseContentDecrypted).decode('utf-8')
responseContentFinal = zlib.decompress(
responseContentDecrypted
).decode("utf-8")
else:
logger.warning(f"[Stage 4] Not Zlib Format!! using raw content: {responseContentDecrypted}")
responseContentFinal = responseContentDecrypted.decode('utf-8')
logger.warning(
f"[Stage 4] Not Zlib Format!! using raw content: {responseContentDecrypted}"
)
responseContentFinal = responseContentDecrypted.decode("utf-8")
# 完成解压
if not noLog:
logger.debug(f"[Stage 4] Process OK, Content: {responseContentFinal}")
logger.debug(
f"[Stage 4] Process OK, Content: {responseContentFinal}"
)
# 最终处理,检查是否是 JSON 格式
if responseContentFinal.startswith('{') and responseContentFinal.endswith('}'):
if responseContentFinal.startswith(
"{"
) and responseContentFinal.endswith("}"):
# 如果是 JSON 格式,直接返回
logger.debug("[Stage 5] Response is JSON, returning.")
return responseContentFinal
else:
# 如果不是 JSON 格式,直接返回但是警告
logger.warning("[Stage 5] Response is not JSON, returning as is, take care!")
logger.warning(
"[Stage 5] Response is not JSON, returning as is, take care!"
)
return responseContentFinal
except:
except Exception:
logger.warning(f"解压失败,原始响应: {responseContentDecrypted}")
raise SDGBResponseError("解压失败")
except SDGBRequestError as e:
@@ -181,16 +207,17 @@ def apiSDGB(
time.sleep(2)
finally:
if 'httpClient' in locals():
if "httpClient" in locals():
httpClient.close()
raise SDGBApiError("重试多次仍然无法成功请求服务器")
def calcPlaySpecial():
"""使用 c_int32 实现的 SpecialNumber 算法"""
rng = random.SystemRandom()
num2 = rng.randint(1, 1037933) * 2069
num2 += 1024 #GameManager.CalcSpecialNum()
num2 += 1024 # GameManager.CalcSpecialNum()
num2 = c_int32(num2).value
result = c_int32(0)
for _ in range(32):
@@ -199,21 +226,24 @@ def calcPlaySpecial():
num2 >>= 1
return c_int32(result.value).value
class AESPKCS7_2024:
# 实现了 maimai 通讯所用的 AES 加密的类
def __init__(self, key: str, iv: str):
self.key = key.encode('utf-8')
self.iv = iv.encode('utf-8')
self.key = key.encode("utf-8")
self.iv = iv.encode("utf-8")
self.mode = AES.MODE_CBC
# 加密
def encrypt(self, content) -> bytes:
# if content is str, convert to bytes
if isinstance(content, str):
encodedData = content.encode('utf-8')
encodedData = content.encode("utf-8")
cipher = AES.new(self.key, self.mode, self.iv)
content_padded = pad(encodedData, AES.block_size)
encrypted_bytes = cipher.encrypt(content_padded)
return encrypted_bytes
# 解密
def decrypt(self, encrypted_content: bytes) -> str:
cipher = AES.new(self.key, self.mode, self.iv)
@@ -221,7 +251,14 @@ class AESPKCS7_2024:
decrypted = unpad(decrypted_padded, AES.block_size)
return decrypted
def apiSDGB_2024(data:str, targetApi:str, userAgentExtraData:str, noLog:bool=False, timeout:int=5):
def apiSDGB_2024(
data: str,
targetApi: str,
userAgentExtraData: str,
noLog: bool = False,
timeout: int = 5,
):
"""
舞萌DX 2024 API 通讯用函数
:param data: 请求数据
@@ -258,13 +295,13 @@ def apiSDGB_2024(data:str, targetApi:str, userAgentExtraData:str, noLog:bool=Fal
"Accept-Encoding": "",
"Charset": "UTF-8",
"Content-Encoding": "deflate",
"Expect": "100-continue"
"Expect": "100-continue",
},
content=reqData_deflated,
# 经测试,加 Verify 之后速度慢好多,因此建议选择性开
#verify=certifi.where(),
#verify=False,
timeout=timeout
# verify=certifi.where(),
# verify=False,
timeout=timeout,
)
if not noLog:
@@ -282,22 +319,22 @@ def apiSDGB_2024(data:str, targetApi:str, userAgentExtraData:str, noLog:bool=Fal
try:
responseDecompressed = zlib.decompress(responseRAWContent)
logger.debug("成功解压响应!")
except:
except Exception:
logger.warning(f"无法解压,得到的原始响应: {responseRAWContent}")
raise SDGBResponseError("解压失败")
try:
resultResponse = aes.decrypt(responseDecompressed)
logger.debug(f"成功解密响应!")
except:
logger.debug("成功解密响应!")
except Exception:
logger.warning(f"解密失败,得到的原始响应: {responseDecompressed}")
raise SDGBResponseError("解密失败")
if not noLog:
logger.debug(f"响应: {resultResponse}")
return resultResponse
# 异常处理
except SDGBRequestError as e:
except SDGBRequestError:
# 请求格式错误,不需要重试
raise SDGBRequestError("请求格式错误")
except SDGBResponseError as e:

View File

@@ -1,45 +1,50 @@
# 改变版本号,实现伪封号和解封号之类
from loguru import logger
from Config import *
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from HelperFullPlay import implFullPlayAction, generateMusicData
from HelperGetUserThing import implGetUser_
from MyConfig import testUid8
def implWipeTickets(userId: int, currentLoginTimestamp:int, currentLoginResult) -> str:
def implWipeTickets(userId: int, currentLoginTimestamp: int, currentLoginResult) -> str:
# Get User Charge
currentUserCharge = implGetUser_("Charge", userId)
currentUserChargeList = currentUserCharge['userChargeList']
currentUserChargeList = currentUserCharge["userChargeList"]
# All stock set to 0
for charge in currentUserChargeList:
charge['stock'] = 0
# example format
# {"userId":11088995,"length":16,"userChargeList":[{"chargeId":1,"stock":0,"purchaseDate":"2025-02-04 00:51:50","validDate":"2025-02-04 00:51:50","extNum1":0},{"chargeId":2,"stock":0,"purchaseDate":"2025-06-11 17:19:42","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":3,"stock":0,"purchaseDate":"2025-06-11 17:19:40","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":4,"stock":0,"purchaseDate":"2025-06-11 09:34:51","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":5,"stock":0,"purchaseDate":"2025-01-30 12:31:16","validDate":"2025-04-30 04:00:00","extNum1":0},{"chargeId":6,"stock":0,"purchaseDate":"2025-02-17 20:01:42","validDate":"2025-02-17 20:01:42","extNum1":0},{"chargeId":7,"stock":0,"purchaseDate":"2025-02-06 16:17:41","validDate":"2025-02-06 16:17:41","extNum1":0},{"chargeId":8,"stock":0,"purchaseDate":"2025-02-06 16:17:49","validDate":"2025-02-06 16:17:49","extNum1":0},{"chargeId":9,"stock":0,"purchaseDate":"2025-02-06 16:18:00","validDate":"2025-02-06 16:18:00","extNum1":0},{"chargeId":10001,"stock":1,"purchaseDate":"2025-06-11 17:19:51","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":10005,"stock":0,"purchaseDate":"2025-04-25 15:45:55","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":10105,"stock":0,"purchaseDate":"2025-04-25 15:46:00","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":10205,"stock":0,"purchaseDate":"2025-04-25 15:46:03","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":11001,"stock":0,"purchaseDate":"2025-01-08 20:43:05","validDate":"2025-04-08 04:00:00","extNum1":0},{"chargeId":30001,"stock":0,"purchaseDate":"2025-04-25 15:46:17","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":999999,"stock":0,"purchaseDate":"2025-02-06 23:03:14","validDate":"2025-02-06 23:03:14","extNum1":0}]}
charge["stock"] = 0
# example format
# {"userId":11088995,"length":16,"userChargeList":[{"chargeId":1,"stock":0,"purchaseDate":"2025-02-04 00:51:50","validDate":"2025-02-04 00:51:50","extNum1":0},{"chargeId":2,"stock":0,"purchaseDate":"2025-06-11 17:19:42","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":3,"stock":0,"purchaseDate":"2025-06-11 17:19:40","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":4,"stock":0,"purchaseDate":"2025-06-11 09:34:51","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":5,"stock":0,"purchaseDate":"2025-01-30 12:31:16","validDate":"2025-04-30 04:00:00","extNum1":0},{"chargeId":6,"stock":0,"purchaseDate":"2025-02-17 20:01:42","validDate":"2025-02-17 20:01:42","extNum1":0},{"chargeId":7,"stock":0,"purchaseDate":"2025-02-06 16:17:41","validDate":"2025-02-06 16:17:41","extNum1":0},{"chargeId":8,"stock":0,"purchaseDate":"2025-02-06 16:17:49","validDate":"2025-02-06 16:17:49","extNum1":0},{"chargeId":9,"stock":0,"purchaseDate":"2025-02-06 16:18:00","validDate":"2025-02-06 16:18:00","extNum1":0},{"chargeId":10001,"stock":1,"purchaseDate":"2025-06-11 17:19:51","validDate":"2025-09-09 04:00:00","extNum1":0},{"chargeId":10005,"stock":0,"purchaseDate":"2025-04-25 15:45:55","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":10105,"stock":0,"purchaseDate":"2025-04-25 15:46:00","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":10205,"stock":0,"purchaseDate":"2025-04-25 15:46:03","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":11001,"stock":0,"purchaseDate":"2025-01-08 20:43:05","validDate":"2025-04-08 04:00:00","extNum1":0},{"chargeId":30001,"stock":0,"purchaseDate":"2025-04-25 15:46:17","validDate":"2025-07-24 04:00:00","extNum1":0},{"chargeId":999999,"stock":0,"purchaseDate":"2025-02-06 23:03:14","validDate":"2025-02-06 23:03:14","extNum1":0}]}
musicData = generateMusicData()
userAllPatches = {
"upsertUserAll": {
# "userData": [{
# "lastRomVersion": romVersion,
# "lastDataVersion": dataVersion
# }],
"userChargeList": currentUserChargeList,
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1" #1避免覆盖
}}
"upsertUserAll": {
# "userData": [{
# "lastRomVersion": romVersion,
# "lastDataVersion": dataVersion
# }],
"userChargeList": currentUserChargeList,
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1", # 1避免覆盖
}
}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
if __name__ == "__main__":
userId = testUid2
userId = testUid8
currentLoginTimestamp = generateTimestamp()
loginResult = apiLogin(currentLoginTimestamp, userId)
if loginResult['returnCode'] != 1:
if loginResult["returnCode"] != 1:
logger.info("登录失败")
exit()
try:
@@ -47,4 +52,4 @@ if __name__ == "__main__":
logger.info(apiLogout(currentLoginTimestamp, userId))
finally:
logger.info(apiLogout(currentLoginTimestamp, userId))
#logger.warning("Error")
# logger.warning("Error")

View File

@@ -5,53 +5,59 @@ import rapidjson as json
from loguru import logger
import xml.etree.ElementTree as ET
from Config import *
from Config import (
loginBonusDBPath,
loginBonusDBPathFallback,
)
from MyConfig import testUid
from API_TitleServer import apiSDGB
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from HelperFullPlay import implFullPlayAction, generateMusicData
class NoSelectedBonusError(Exception):
pass
def apiQueryLoginBonus(userId:int) -> str:
def apiQueryLoginBonus(userId: int) -> str:
"""ログインボーナスを取得する API"""
data = json.dumps({
"userId": int(userId),
"nextIndex": 0,
"maxCount": 2000
})
data = json.dumps({"userId": int(userId), "nextIndex": 0, "maxCount": 2000})
return apiSDGB(data, "GetUserLoginBonusApi", userId)
def implLoginBonus(userId: int, currentLoginTimestamp:int, currentLoginResult, bonusGenerateMode=1):
def implLoginBonus(
userId: int, currentLoginTimestamp: int, currentLoginResult, bonusGenerateMode=1
):
"""
ログインボーナスデータをアップロードする
bonusGenerateMode は、ログインボーナスを生成する方法を指定します。
1: 選択したボーナスのみ MAX にする(選択したボーナスはないの場合は False を返す)
2: 全部 MAX にする
"""
musicData = generateMusicData()
musicData = generateMusicData()
# サーバーからログインボーナスデータを取得
data = json.dumps({
"userId": int(userId),
"nextIndex": 0,
"maxCount": 2000
})
data = json.dumps({"userId": int(userId), "nextIndex": 0, "maxCount": 2000})
UserLoginBonusResponse = json.loads(apiSDGB(data, "GetUserLoginBonusApi", userId))
# ログインボーナスリストを生成、それから処理してアップロード
UserLoginBonusList = UserLoginBonusResponse['userLoginBonusList']
UserLoginBonusList = UserLoginBonusResponse["userLoginBonusList"]
finalBonusList = generateLoginBonusList(UserLoginBonusList, bonusGenerateMode)
if not finalBonusList:
return False #ログインボーナスを選択していないから失敗
return False # ログインボーナスを選択していないから失敗
# UserAllのパッチ
userAllPatches = {
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1", #上書きしない
"userLoginBonusList": finalBonusList,
"isNewLoginBonusList": "0" * len(finalBonusList)
}}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1", # 上書きしない
"userLoginBonusList": finalBonusList,
"isNewLoginBonusList": "0" * len(finalBonusList),
}
}
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
def generateLoginBonusList(UserLoginBonusList, generateMode=1):
"""
ログインボーナスリストを生成します。
@@ -65,60 +71,64 @@ def generateLoginBonusList(UserLoginBonusList, generateMode=1):
try:
tree = ET.parse(loginBonusDBPath)
root = tree.getroot()
loginBonusIdList = [int(item.find('id').text) for item in root.findall('.//StringID')]
loginBonusIdList = [
int(item.find("id").text) for item in root.findall(".//StringID")
]
logger.debug(f"ログインボーナスIDリスト: {loginBonusIdList}")
except FileNotFoundError:
try:
tree = ET.parse(loginBonusDBPathFallback)
root = tree.getroot()
loginBonusIdList = [int(item.find('id').text) for item in root.findall('.//StringID')]
loginBonusIdList = [
int(item.find("id").text) for item in root.findall(".//StringID")
]
logger.debug(f"ログインボーナスIDリスト: {loginBonusIdList}")
except:
except Exception:
raise FileNotFoundError("ログインボーナスデータベースを読み込めません")
# ログインボーナスの MAX POINT は5の場合があります
# その全部のボーナスIDをこのリストに追加してください
# 必ず最新のデータを使用してください
Bonus5Id = [12, 29, 30, 38, 43, 604, 611]
# UserBonusList から bonusId を取得
UserLoginBonusIdList = [item['bonusId'] for item in UserLoginBonusList]
UserLoginBonusIdList = [item["bonusId"] for item in UserLoginBonusList]
# 存在しないボーナス
NonExistingBonuses = list(set(loginBonusIdList) - set(UserLoginBonusIdList))
logger.debug(f"存在しないボーナス: {NonExistingBonuses}")
bonusList = []
if generateMode == 1: #選択したボーナスのみ MAX にする
if generateMode == 1: # 選択したボーナスのみ MAX にする
for item in UserLoginBonusList:
if item['isCurrent'] and not item['isComplete']:
point = 4 if item['bonusId'] in Bonus5Id else 9
if item["isCurrent"] and not item["isComplete"]:
point = 4 if item["bonusId"] in Bonus5Id else 9
data = {
"bonusId": item['bonusId'],
"bonusId": item["bonusId"],
"point": point,
"isCurrent": True,
"isComplete": False
"isComplete": False,
}
bonusList.append(data)
if len(bonusList) == 0:
raise NoSelectedBonusError("選択したログインボーナスがありません")
elif generateMode == 2: #全部 MAX にする
elif generateMode == 2: # 全部 MAX にする
# 存在しているボーナスを追加
for item in UserLoginBonusList:
if not item['isComplete']:
if not item["isComplete"]:
data = {
"bonusId": item['bonusId'],
"point": 4 if item['bonusId'] in Bonus5Id else 9,
"isCurrent": item['isCurrent'],
"isComplete": False
"bonusId": item["bonusId"],
"point": 4 if item["bonusId"] in Bonus5Id else 9,
"isCurrent": item["isCurrent"],
"isComplete": False,
}
bonusList.append(data)
elif item['bonusId'] == 999:
elif item["bonusId"] == 999:
data = {
"bonusId": 999,
"point": (item['point'] // 10) * 10 + 9,
"isCurrent": item['isCurrent'],
"isComplete": False
"point": (item["point"] // 10) * 10 + 9,
"isCurrent": item["isCurrent"],
"isComplete": False,
}
bonusList.append(data)
# 存在しないボーナスを追加
@@ -127,19 +137,20 @@ def generateLoginBonusList(UserLoginBonusList, generateMode=1):
"bonusId": bonusId,
"point": 4 if bonusId in Bonus5Id else 9,
"isCurrent": False,
"isComplete": False
"isComplete": False,
}
bonusList.append(data)
else:
raise SyntaxError("generateMode は 1 または 2 でなければなりません")
logger.debug(f"ログインボーナスリスト: {bonusList}")
return bonusList
if __name__ == "__main__":
# ログインボーナスデータをアップロードする
userId = testUid
currentLoginTimestamp = generateTimestamp()
currentLoginResult = apiLogin(currentLoginTimestamp, userId)
implLoginBonus(userId, currentLoginTimestamp, currentLoginResult, 2)
apiLogout(currentLoginTimestamp, userId)
apiLogout(currentLoginTimestamp, userId)

View File

@@ -1,71 +1,99 @@
# 删除和上传成绩
from loguru import logger
from Config import *
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from HelperFullPlay import implFullPlayAction
from MyConfig import testUid
def implDeleteMusicRecord(userId: int, currentLoginTimestamp:int, currentLoginResult, musicId:int, levelId:int) -> str:
musicData= ({
"musicId": musicId,
"level": levelId,
"playCount": 1,
"achievement": 0,
"comboStatus": 0,
"syncStatus": 0,
"deluxscoreMax": 0,
"scoreRank": 0,
"extNum1": 0
})
def implDeleteMusicRecord(
userId: int,
currentLoginTimestamp: int,
currentLoginResult,
musicId: int,
levelId: int,
) -> str:
musicData = {
"musicId": musicId,
"level": levelId,
"playCount": 1,
"achievement": 0,
"comboStatus": 0,
"syncStatus": 0,
"deluxscoreMax": 0,
"scoreRank": 0,
"extNum1": 0,
}
userAllPatches = {
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "0" # 0为编辑即可删除掉成绩
}}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "0", # 0为编辑即可删除掉成绩
}
}
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
def implUploadMusicRecord(userId: int, currentLoginTimestamp:int, currentLoginResult, musicId:int, levelId:int, achievement:int, dxScore:int) -> str:
def implUploadMusicRecord(
userId: int,
currentLoginTimestamp: int,
currentLoginResult,
musicId: int,
levelId: int,
achievement: int,
dxScore: int,
) -> str:
"""
VERY EARLY STAGE OF UPLOADING SCORES!!!! DO NOT USE THIS!!!!
上传成绩的参考实现。
"""
# 要上传的数据
musicData= ({
"musicId": musicId,
"level": levelId,
"playCount": 1,
"achievement": achievement,
"comboStatus": 0,
"syncStatus": 0,
"deluxscoreMax": dxScore,
"scoreRank": 0,
"extNum1": 0
})
musicData = {
"musicId": musicId,
"level": levelId,
"playCount": 1,
"achievement": achievement,
"comboStatus": 0,
"syncStatus": 0,
"deluxscoreMax": dxScore,
"scoreRank": 0,
"extNum1": 0,
}
userAllPatches = {
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1" # 0编辑 1插入
}}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1", # 0编辑 1插入
}
}
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
if __name__ == "__main__":
userId = testUid
currentLoginTimestamp = generateTimestamp()
loginResult = apiLogin(currentLoginTimestamp, userId)
musicId = 852 #852 is tiamat
levelId = 3 #3 is MASTER
musicId = 852 # 852 is tiamat
levelId = 3 # 3 is MASTER
if loginResult['returnCode'] != 1:
if loginResult["returnCode"] != 1:
logger.info("登录失败")
exit()
try:
logger.info(implDeleteMusicRecord(userId, currentLoginTimestamp, loginResult, musicId, levelId))
#logger.info(implUploadMusicRecord(userId, currentLoginTimestamp, loginResult, musicId, levelId, 1000000, 100))
logger.info(
implDeleteMusicRecord(
userId, currentLoginTimestamp, loginResult, musicId, levelId
)
)
# logger.info(implUploadMusicRecord(userId, currentLoginTimestamp, loginResult, musicId, levelId, 1000000, 100))
logger.info(apiLogout(currentLoginTimestamp, userId))
finally:
logger.info(apiLogout(currentLoginTimestamp, userId))
#logger.warning("Error")
# logger.warning("Error")

View File

@@ -2,57 +2,59 @@
from loguru import logger
from Config import *
from MyConfig import testUid8
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from HelperUnlockThing import implUnlockThing
def implUnlockSingleItem(itemId: int, itemKind: int, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str:
def implUnlockSingleItem(
itemId: int,
itemKind: int,
userId: int,
currentLoginTimestamp: int,
currentLoginResult,
) -> str:
"""
发单个东西,比如搭档 10
"""
userItemList = [
{
"itemKind": itemKind,
"itemId": itemId,
"stock": 1,
"isValid": True
}
{"itemKind": itemKind, "itemId": itemId, "stock": 1, "isValid": True}
]
unlockThingResult = implUnlockThing(userItemList, userId, currentLoginTimestamp, currentLoginResult)
unlockThingResult = implUnlockThing(
userItemList, userId, currentLoginTimestamp, currentLoginResult
)
return unlockThingResult
def implUnlockMusic(musicToBeUnlocked: int, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str:
def implUnlockMusic(
musicToBeUnlocked: int, userId: int, currentLoginTimestamp: int, currentLoginResult
) -> str:
"""
解锁乐曲
"""
userItemList = [
{
"itemKind": 5,
"itemId": musicToBeUnlocked,
"stock": 1,
"isValid": True
},
{
"itemKind": 6,
"itemId": musicToBeUnlocked,
"stock": 1,
"isValid": True
},
{"itemKind": 5, "itemId": musicToBeUnlocked, "stock": 1, "isValid": True},
{"itemKind": 6, "itemId": musicToBeUnlocked, "stock": 1, "isValid": True},
]
unlockThingResult = implUnlockThing(userItemList, userId, currentLoginTimestamp, currentLoginResult)
unlockThingResult = implUnlockThing(
userItemList, userId, currentLoginTimestamp, currentLoginResult
)
return unlockThingResult
if __name__ == "__main__":
userId = testUid2
userId = int(input("type user id: ").strip() or "0") or testUid8
currentLoginTimestamp = generateTimestamp()
loginResult = apiLogin(currentLoginTimestamp, userId)
if loginResult['returnCode'] != 1:
if loginResult["returnCode"] != 1:
logger.info("登录失败")
exit()
try:
logger.info(implWipeTickets(userId, currentLoginTimestamp, loginResult))
logger.info(
implUnlockSingleItem(14, 10, userId, currentLoginTimestamp, loginResult)
)
logger.info(apiLogout(currentLoginTimestamp, userId))
finally:
logger.info(apiLogout(currentLoginTimestamp, userId))
#logger.warning("Error")
# logger.warning("Error")

View File

@@ -1,57 +1,56 @@
from API_TitleServer import *
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from Config import *
import requests
from loguru import logger
from HelperGetUserMusicDetail import getUserFullMusicDetail
from HelperMusicDB import getMusicTitle
import requests
import rapidjson as json
class divingFishAuthFailError(Exception):
pass
class divingFishCommError(Exception):
pass
# 水鱼查分器的 API 地址
BASE_URL = 'https://www.diving-fish.com/api/maimaidxprober'
BASE_URL = "https://www.diving-fish.com/api/maimaidxprober"
# 水鱼查分器的成绩状态转换
COMBO_ID_TO_NAME = ['', 'fc', 'fcp', 'ap', 'app']
SYNC_ID_TO_NAME = ['', 'fs', 'fsp', 'fsd', 'fsdp', 'sync']
COMBO_ID_TO_NAME = ["", "fc", "fcp", "ap", "app"]
SYNC_ID_TO_NAME = ["", "fs", "fsp", "fsd", "fsdp", "sync"]
def apiDivingFish(method:str, apiPath:str, importToken:str, data=None):
'''水鱼查分器的 API 通讯实现'''
headers = {
"Import-Token": importToken
}
if method == 'POST':
headers['Content-Type'] = 'application/json'
logger.info(f'水鱼查分器 API 请求:{method} {BASE_URL + apiPath}')
if method == 'POST':
def apiDivingFish(method: str, apiPath: str, importToken: str, data=None):
"""水鱼查分器的 API 通讯实现"""
headers = {"Import-Token": importToken}
if method == "POST":
headers["Content-Type"] = "application/json"
logger.info(f"水鱼查分器 API 请求:{method} {BASE_URL + apiPath}")
if method == "POST":
response = requests.post(
url=BASE_URL + apiPath,
json=data,
headers=headers,
)
elif method == 'GET':
elif method == "GET":
response = requests.get(
url=BASE_URL + apiPath,
headers=headers,
)
elif method == 'DELETE':
elif method == "DELETE":
response = requests.delete(
url=BASE_URL + apiPath,
headers=headers,
)
else:
logger.error(f'未知的请求方法:{method}')
raise ValueError(f'未知的请求方法:{method}')
logger.info(f'水鱼查分器请求结果:{response.status_code}')
logger.debug(f'水鱼查分器回应:{response.text}')
finalResponseTextDecode = response.text.encode('utf-8').decode('unicode_escape')
logger.debug(f'水鱼查分器回应解码后:{finalResponseTextDecode}')
logger.error(f"未知的请求方法:{method}")
raise ValueError(f"未知的请求方法:{method}")
logger.info(f"水鱼查分器请求结果:{response.status_code}")
logger.debug(f"水鱼查分器回应:{response.text}")
finalResponseTextDecode = response.text.encode("utf-8").decode("unicode_escape")
logger.debug(f"水鱼查分器回应解码后:{finalResponseTextDecode}")
match response.status_code:
case 200:
return response.json()
@@ -60,89 +59,103 @@ def apiDivingFish(method:str, apiPath:str, importToken:str, data=None):
case _:
raise divingFishCommError
def getFishRecords(importToken: str) -> dict:
'''获取水鱼查分器的成绩'''
return apiDivingFish('GET', '/player/records', importToken)
"""获取水鱼查分器的成绩"""
return apiDivingFish("GET", "/player/records", importToken)
def updateFishRecords(importToken: str, records: list[dict]) -> dict:
'''上传成绩到水鱼查分器'''
return apiDivingFish('POST', '/player/update_records', importToken, records)
"""上传成绩到水鱼查分器"""
return apiDivingFish("POST", "/player/update_records", importToken, records)
def resetFishRecords(fishImportToken:str):
'''重置水鱼查分器的用户数据'''
return apiDivingFish('DELETE', '/player/delete_records', fishImportToken)
def getFishUserInfo(userQQ:int):
'''按QQ获取水鱼查分器的用户信息'''
return apiDivingFish('POST', '/query/player', "", {"qq": userQQ})
def resetFishRecords(fishImportToken: str):
"""重置水鱼查分器的用户数据"""
return apiDivingFish("DELETE", "/player/delete_records", fishImportToken)
def getFishUserInfo(userQQ: int):
"""按QQ获取水鱼查分器的用户信息"""
return apiDivingFish("POST", "/query/player", "", {"qq": userQQ})
def maimaiUserMusicDetailToDivingFishFormat(userMusicDetailList) -> list:
'''舞萌的 UserMusicDetail 成绩格式转换成水鱼的格式'''
"""舞萌的 UserMusicDetail 成绩格式转换成水鱼的格式"""
divingFishList = []
for currentMusicDetail in userMusicDetailList:
# musicId 大于 100000 属于宴谱,不计入
if currentMusicDetail['musicId'] >= 100000:
if currentMusicDetail["musicId"] >= 100000:
continue
# 获得歌名
currentMusicTitle = getMusicTitle(currentMusicDetail['musicId'])
currentMusicTitle = getMusicTitle(currentMusicDetail["musicId"])
# 如果数据库里未找到此歌曲
if currentMusicTitle == "R_ERR_MUSIC_ID_NOT_IN_DATABASE":
logger.warning(f"数据库无此歌曲 跳过: {currentMusicDetail['musicId']}")
continue
# 每一个乐曲都判断下是 DX 还是标准
if currentMusicDetail['musicId'] >= 10000:
notesType = 'DX'
if currentMusicDetail["musicId"] >= 10000:
notesType = "DX"
else:
notesType = 'SD'
notesType = "SD"
# 追加进列表
try:
divingFishList.append({
'achievements': (currentMusicDetail['achievement'] / 10000), # 水鱼的成绩是 float 而非舞萌的 int
'title': currentMusicTitle,
'type': notesType,
'level_index': currentMusicDetail['level'],
'fc': COMBO_ID_TO_NAME[currentMusicDetail['comboStatus']],
'fs': SYNC_ID_TO_NAME[currentMusicDetail['syncStatus']],
'dxScore': currentMusicDetail['deluxscoreMax'],
})
except:
divingFishList.append(
{
"achievements": (
currentMusicDetail["achievement"] / 10000
), # 水鱼的成绩是 float 而非舞萌的 int
"title": currentMusicTitle,
"type": notesType,
"level_index": currentMusicDetail["level"],
"fc": COMBO_ID_TO_NAME[currentMusicDetail["comboStatus"]],
"fs": SYNC_ID_TO_NAME[currentMusicDetail["syncStatus"]],
"dxScore": currentMusicDetail["deluxscoreMax"],
}
)
except Exception:
logger.error(f"无法将 UserMusic 翻译成水鱼格式: {currentMusicDetail}")
return divingFishList
def isVaildFishToken(importToken:str):
'''通过尝试获取一次成绩,检查水鱼查分器的 Token 是否有效
有效返回 True无效返回 False'''
result = apiDivingFish('GET', '/player/records', importToken)
def isVaildFishToken(importToken: str):
"""通过尝试获取一次成绩,检查水鱼查分器的 Token 是否有效
有效返回 True无效返回 False"""
result = apiDivingFish("GET", "/player/records", importToken)
logger.debug(f"水鱼查分器 Token 检查结果:{result}")
if result:
return True
return False
def implGetUserCurrentDXRating(userQQ:int):
'''获取用户当前的 DX RATING'''
def implGetUserCurrentDXRating(userQQ: int):
"""获取用户当前的 DX RATING"""
try:
playerData = getFishUserInfo(userQQ)
playerRating = playerData['rating']
playerRating = playerData["rating"]
logger.info(f"用户 {userQQ} 的 DX RATING 是 {playerRating}")
except Exception as e:
logger.warning(f"无法获取用户 {userQQ} 的 DX RATING: {e}")
return False
return playerRating
def implUserMusicToDivingFish(userId:int, fishImportToken:str):
'''上传所有成绩到水鱼的参考实现。
def implUserMusicToDivingFish(userId: int, fishImportToken: str):
"""上传所有成绩到水鱼的参考实现。
返回一个 int 的 ErrorCode。
0: Success
1: Get User Music Fail
2: Auth Fail
3: Comm Error
'''
"""
logger.info("开始尝试上传舞萌成绩到水鱼查分器!")
try:
userFullMusicDetailList = getUserFullMusicDetail(userId)
logger.info("成功得到成绩!转换成水鱼格式..")
divingFishData = maimaiUserMusicDetailToDivingFishFormat(userFullMusicDetailList)
divingFishData = maimaiUserMusicDetailToDivingFishFormat(
userFullMusicDetailList
)
logger.info("转换成功!开始上传水鱼..")
except Exception as e:
logger.error(f"获取成绩失败!{e}")
@@ -156,8 +169,9 @@ def implUserMusicToDivingFish(userId:int, fishImportToken:str):
logger.error("水鱼查分器通讯失败!")
return 3
def generateDebugTestScore():
'''生成测试成绩'''
"""生成测试成绩"""
return [
{
"achievement": 1010000,
@@ -165,7 +179,7 @@ def generateDebugTestScore():
"deluxscoreMax": 4026,
"level": 4,
"musicId": 834,
"syncStatus": 4
"syncStatus": 4,
},
{
"achievement": 1010000,
@@ -173,7 +187,6 @@ def generateDebugTestScore():
"deluxscoreMax": 4200,
"level": 4,
"musicId": 11663,
"syncStatus": 4
}
"syncStatus": 4,
},
]

View File

@@ -1,106 +1,126 @@
from datetime import datetime, timedelta
# 倍票相关 API 的实现
import rapidjson as json
import pytz
from datetime import datetime, timedelta
from loguru import logger
from Config import *
from API_TitleServer import apiSDGB
from HelperGetUserThing import implGetUser_
from loguru import logger
from Config import (
clientId,
placeId,
regionId,
)
from MyConfig import testUid2
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from HelperFullPlay import implFullPlayAction, generateMusicData
from HelperGetUserThing import implGetUser_
def implWipeTickets(userId: int, currentLoginTimestamp:int, currentLoginResult) -> str:
'''清空用户所有票的 API 请求器,返回 Json String。'''
def implWipeTickets(userId: int, currentLoginTimestamp: int, currentLoginResult) -> str:
"""清空用户所有票的 API 请求器,返回 Json String。"""
# 先得到当前用户的 Charge 数据
currentUserCharge = implGetUser_("Charge", userId)
# 取得 List
currentUserChargeList = currentUserCharge['userChargeList']
currentUserChargeList = currentUserCharge["userChargeList"]
# 所有 stock 都置为 0
for charge in currentUserChargeList:
charge['stock'] = 0
charge["stock"] = 0
musicData = generateMusicData()
userAllPatches = {
"upsertUserAll": {
"userChargeList": currentUserChargeList,
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1" #1避免覆盖
}}
"upsertUserAll": {
"userChargeList": currentUserChargeList,
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1", # 1避免覆盖
}
}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
def apiQueryTicket(userId:int) -> str:
'''查询已有票的 API 请求器,返回 Json String。'''
def apiQueryTicket(userId: int) -> str:
"""查询已有票的 API 请求器,返回 Json String。"""
# 构建 Payload
data = json.dumps({
"userId": userId
})
data = json.dumps({"userId": userId})
# 发送请求
userdata_result = apiSDGB(data, "GetUserChargeApi", userId)
# 返回响应
return userdata_result
def apiBuyTicket(userId:int, ticketType:int, price:int, playerRating:int, playCount:int) -> str:
'''倍票购买 API 的请求器'''
nowTime = datetime.now(pytz.timezone('Asia/Shanghai'))
def apiBuyTicket(
userId: int, ticketType: int, price: int, playerRating: int, playCount: int
) -> str:
"""倍票购买 API 的请求器"""
nowTime = datetime.now(pytz.timezone("Asia/Shanghai"))
# 构造请求数据 Payload
data = json.dumps({
"userId": userId,
"userChargelog": {
"chargeId": ticketType,
"price": price,
"purchaseDate": nowTime.strftime("%Y-%m-%d %H:%M:%S.0"),
"playCount": playCount,
"playerRating": playerRating,
"placeId": placeId,
"regionId": regionId,
"clientId": clientId
},
"userCharge": {
"chargeId": ticketType,
"stock": 1,
"purchaseDate": nowTime.strftime("%Y-%m-%d %H:%M:%S.0"),
"validDate": (nowTime + timedelta(days=90)).replace(hour=4, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S")
data = json.dumps(
{
"userId": userId,
"userChargelog": {
"chargeId": ticketType,
"price": price,
"purchaseDate": nowTime.strftime("%Y-%m-%d %H:%M:%S.0"),
"playCount": playCount,
"playerRating": playerRating,
"placeId": placeId,
"regionId": regionId,
"clientId": clientId,
},
"userCharge": {
"chargeId": ticketType,
"stock": 1,
"purchaseDate": nowTime.strftime("%Y-%m-%d %H:%M:%S.0"),
"validDate": (nowTime + timedelta(days=90))
.replace(hour=4, minute=0, second=0)
.strftime("%Y-%m-%d %H:%M:%S"),
},
}
})
)
# 发送请求,返回最终得到的 Json String 回执
return apiSDGB(data, "UpsertUserChargelogApi", userId)
def implBuyTicket(userId:int, ticketType:int):
'''
def implBuyTicket(userId: int, ticketType: int):
"""
购买倍票 API 的参考实现。
需要事先登录.
返回服务器响应的 Json string。
'''
"""
# 先使用 GetUserData API 请求器,取得 rating 和 pc 数
currentUserData = implGetUser_("Data", userId)
if currentUserData:
playerRating = currentUserData['userData']['playerRating']
playCount = currentUserData['userData'].get('playCount', 0)
playerRating = currentUserData["userData"]["playerRating"]
playCount = currentUserData["userData"].get("playCount", 0)
else:
return False
# 正式买票
getTicketResponseStr = apiBuyTicket(userId, ticketType, ticketType-1, playerRating, playCount)
getTicketResponseStr = apiBuyTicket(
userId, ticketType, ticketType - 1, playerRating, playCount
)
# 返回结果
return getTicketResponseStr
if __name__ == "__main__":
userId = testUid2
currentLoginTimestamp = generateTimestamp()
loginResult = apiLogin(currentLoginTimestamp, userId)
if loginResult['returnCode'] != 1:
if loginResult["returnCode"] != 1:
logger.info("登录失败")
exit()
try:
logger.info(implBuyTicket(userId, 2)) # 购买倍票
#logger.info(apiQueryTicket(userId))
# logger.info(apiQueryTicket(userId))
finally:
logger.info(apiLogout(currentLoginTimestamp, userId))
#logger.warning("Error")
# logger.warning("Error")

View File

@@ -14,4 +14,3 @@ loginBonusDBPathFallback = "./maimaiDX-Api/Data/loginBonusDB.xml"
musicDBPathFallback = "./maimaiDX-Api/Data/musicDB.json"
# 日本精工,安全防漏
#from MyConfig import *

View File

@@ -1,36 +1,41 @@
# 解小黑屋实现
# 仍十分不完善,不建议使用
from Config import *
from API_TitleServer import *
from GetPreview import apiGetUserPreview
from HelperLogInOut import apiLogout
import rapidjson as json
from loguru import logger
import time
from datetime import datetime
import asyncio
import rapidjson as json
from GetPreview import apiGetUserPreview
from HelperLogInOut import apiLogout
from loguru import logger
from MyConfig import testUid2
def isUserLoggedIn(userId):
isLogin = json.loads(apiGetUserPreview(userId, True))['isLogin']
isLogin = json.loads(apiGetUserPreview(userId, True))["isLogin"]
logger.debug(f"用户 {userId} 是否登录: {isLogin}")
return isLogin
def getHumanReadableTime(unixTime):
'''将 Unix 时间戳转换为人类可读的时间'''
"""将 Unix 时间戳转换为人类可读的时间"""
# 减一个小时,因为舞萌貌似是 UTC+9
timestamp = int(unixTime) - 3600
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
def getMaimaiUNIXTime(mmddhhmmss, year=2025):
"""
解析用户输入的 MMDDHHMMSS 格式的时间,返回 Unix 时间戳
时间会被推后一个小时,因为舞萌貌似是 UTC+9
"""
#date_time_str = f"{year}{mmddhhmmss}"
# date_time_str = f"{year}{mmddhhmmss}"
# 加上一个小时
date_time_str = f"{year}{mmddhhmmss[:4]}{int(mmddhhmmss[4:6])+1:02}{mmddhhmmss[6:]}"
date_time_obj = datetime.strptime(date_time_str, '%Y%m%d%H%M%S')
date_time_str = (
f"{year}{mmddhhmmss[:4]}{int(mmddhhmmss[4:6]) + 1:02}{mmddhhmmss[6:]}"
)
date_time_obj = datetime.strptime(date_time_str, "%Y%m%d%H%M%S")
# 将 datetime 对象转换为 Unix 时间戳
unix_timestamp = int(time.mktime(date_time_obj.timetuple()))
logger.info(f"转换出了时间戳: {unix_timestamp}")
@@ -41,19 +46,20 @@ def logOut(userId, Timestamp):
"""极其简单的登出实现,成功返回 True失败返回 False
注意:不会检查用户是否真的登出了,只会尝试登出"""
try:
if apiLogout(Timestamp, userId, True)['returnCode'] == 1:
if apiLogout(Timestamp, userId, True)["returnCode"] == 1:
# 成功送出了登出请求
logger.debug(f"已成功尝试登出用户 {userId}")
return True
except:
except Exception:
logger.error(f"登出用户 {userId} 的时候发生了错误")
return False
def isCorrectTimestamp(timestamp, userId):
'''
"""
动作:给定一个时间戳,用它尝试登出用户,然后检查用户是否成功登出。
如果用户成功登出,返回 True否则返回 False。
'''
"""
if not logOut(userId, timestamp):
logger.error(f"用时间戳 {timestamp} 登出用户 {userId} 的时候发生了错误")
return False
@@ -61,6 +67,7 @@ def isCorrectTimestamp(timestamp, userId):
logger.debug(f"时间戳 {timestamp} 是否正确: {isLoggedOut}")
return isLoggedOut
def findCorrectTimestamp(timestamp, userId, max_attempts=600):
# 初始化偏移量
offset = 1
@@ -72,19 +79,19 @@ def findCorrectTimestamp(timestamp, userId, max_attempts=600):
if isCorrectTimestamp(currentTryTimestamp, userId):
logger.info(f"正确的时间戳: {currentTryTimestamp}")
return currentTryTimestamp
# 增加偏移量尝试
currentTryTimestamp = timestamp + offset
if isCorrectTimestamp(currentTryTimestamp, userId):
logger.info(f"正确的时间戳(在给定时间以后): {currentTryTimestamp}")
return currentTryTimestamp
# 减少偏移量尝试
currentTryTimestamp = timestamp - offset
if isCorrectTimestamp(currentTryTimestamp, userId):
logger.info(f"正确的时间戳(在给定时间以前): {currentTryTimestamp}")
return currentTryTimestamp
# 增加尝试次数和偏移量
attempts += 2
offset += 1
@@ -92,6 +99,7 @@ def findCorrectTimestamp(timestamp, userId, max_attempts=600):
logger.error(f"无法找到正确的时间戳,尝试次数超过了 {max_attempts}")
return None
if __name__ == "__main__":
human_time = "0207155500"
beginTimestamp = getMaimaiUNIXTime(human_time)

View File

@@ -2,56 +2,17 @@
import rapidjson as json
from API_TitleServer import apiSDGB
from Config import *
import time
import random
from loguru import logger
def apiGetUserPreview(userId, noLog:bool=False) -> str:
data = json.dumps({
"userId": int(userId)
})
def apiGetUserPreview(userId, noLog: bool = False) -> str:
data = json.dumps({"userId": int(userId)})
preview_result = apiSDGB(data, "GetUserPreviewApi", userId, noLog)
return preview_result
# CLI 示例
if __name__ == "__main__":
#userId = input("请输入用户 ID")
userId = testUid8
userId = input("请输入用户 ID")
# userId = testUid8
print(apiGetUserPreview(userId))
###
### 以下仅留作归档
###
def crawlAllUserPreview():
"""omg it's a evil crawler"""
# 这里设置开始和结束的 UserId
BeginUserId = 10200000
EndUserId = 12599999
# 打开文件,准备写入
with open('Remi_UserID_DB_Output.txt', 'w', encoding="utf-8") as f:
# 遍历 UserId
for userId in range(BeginUserId, EndUserId + 1):
# 调用 API
try:
userPreview = apiGetUserPreview(userId, True)
currentUser = json.loads(userPreview)
if currentUser["userId"] is not None:
# 每爬到一个就把它存到一个文件里面,每个一行
f.write(userPreview + "\n")
logger.info(f"{userId}: {currentUser['userName']}, RATING: {currentUser['playerRating']}")
else:
f.write("\n")
except:
f.write("ERROR\n")
time.sleep(4)
f.flush()
# 随机等待0.2-0.5秒
time.sleep(random.uniform(0.2, 0.5))
print('Finished!')
#if __name__ == "__main__":
# crawlAllUserPreview()
print(apiSDGB("{}", "Ping", userId, False))

21
GetPreview2024.py Normal file
View File

@@ -0,0 +1,21 @@
# 获取用户简略预览数据的 API 实现,此 API 无需任何登录即可调取
import rapidjson as json
from API_TitleServer import apiSDGB_2024, use2024Api
use2024Api()
def apiGetUserPreview(userId, noLog: bool = False) -> str:
data = json.dumps({"userId": int(userId)})
preview_result = apiSDGB_2024(data, "GetUserPreviewApi", userId, noLog)
return preview_result
# CLI 示例
if __name__ == "__main__":
userId = input("请输入用户 ID")
# userId = testUid8
# print(apiGetUserPreview(userId))
print(apiSDGB_2024("{}", "Ping", userId, False))

View File

@@ -1,16 +1,21 @@
import rapidjson as json
from loguru import logger
from Config import *
from API_TitleServer import *
from API_TitleServer import (
SDGBRequestError,
SDGBApiError,
apiSDGB,
calcPlaySpecial,
)
from HelperGetUserThing import implGetUser_
from HelperUploadUserPlayLog import apiUploadUserPlaylog
from HelperUserAll import generateFullUserAll
def generateMusicData():
"""生成一份占位的音乐数据"""
return {
"musicId": 834, # PANDORA PARADOXXX
"musicId": 834, # PANDORA PARADOXXX
"level": 4,
"playCount": 1,
"achievement": 0,
@@ -18,9 +23,10 @@ def generateMusicData():
"syncStatus": 0,
"deluxscoreMax": 0,
"scoreRank": 0,
"extNum1": 0
"extNum1": 0,
}
def applyUserAllPatches(userAll, patches):
"""
递归地将给定的补丁应用到用户数据的各个层次。
@@ -29,13 +35,25 @@ def applyUserAllPatches(userAll, patches):
:param patches: 包含所有patch的字典
"""
for key, value in patches.items():
if isinstance(value, dict) and key in userAll and isinstance(userAll[key], dict):
if (
isinstance(value, dict)
and key in userAll
and isinstance(userAll[key], dict)
):
# 如果patch的值是字典并且userAll中对应的key也是字典递归处理
applyUserAllPatches(userAll[key], value)
elif isinstance(value, list) and key in userAll and isinstance(userAll[key], list):
elif (
isinstance(value, list)
and key in userAll
and isinstance(userAll[key], list)
):
# 如果值是列表,进行详细的更新处理
for i, patch_item in enumerate(value):
if i < len(userAll[key]) and isinstance(patch_item, dict) and isinstance(userAll[key][i], dict):
if (
i < len(userAll[key])
and isinstance(patch_item, dict)
and isinstance(userAll[key][i], dict)
):
# 如果列表项是字典,更新字典中的字段
applyUserAllPatches(userAll[key][i], patch_item)
elif i >= len(userAll[key]):
@@ -44,19 +62,29 @@ def applyUserAllPatches(userAll, patches):
else:
# 否则直接更新或添加key
userAll[key] = value
def implFullPlayAction(userId: int, currentLoginTimestamp:int, currentLoginResult, musicData, userAllPatches, debugMode=False):
def implFullPlayAction(
userId: int,
currentLoginTimestamp: int,
currentLoginResult,
musicData,
userAllPatches,
debugMode=False,
):
"""
一份完整的上机实现,可以打 patch 来实现各种功能
需要在外部先登录并传入登录结果
"""
# 取得 UserData
currentUserData = implGetUser_("Data", userId)
currentUserData2 = currentUserData['userData']
currentUserData2 = currentUserData["userData"]
# 构建并上传一个游玩记录
currentUploadUserPlaylogApiResult = apiUploadUserPlaylog(userId, musicData, currentUserData2, currentLoginResult['loginId'])
currentUploadUserPlaylogApiResult = apiUploadUserPlaylog(
userId, musicData, currentUserData2, currentLoginResult["loginId"]
)
logger.debug(f"上传 UserPlayLog 结果: {currentUploadUserPlaylogApiResult}")
# 构建并上传 UserAll
@@ -65,13 +93,22 @@ def implFullPlayAction(userId: int, currentLoginTimestamp:int, currentLoginResul
# 计算一个特殊数
currentPlaySpecial = calcPlaySpecial()
# 生成出 UserAll
currentUserAll = generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial)
currentUserAll = generateFullUserAll(
userId,
currentLoginResult,
currentLoginTimestamp,
currentUserData2,
currentPlaySpecial,
)
# 应用参数里的补丁
applyUserAllPatches(currentUserAll, userAllPatches)
# 调试模式下直接输出数据
if debugMode:
logger.debug("调试模式:构建出的 UserAll 数据:" + json.dumps(currentUserAll, indent=4))
logger.debug(
"调试模式:构建出的 UserAll 数据:"
+ json.dumps(currentUserAll, indent=4)
)
logger.info("Bye!")
return
@@ -88,8 +125,8 @@ def implFullPlayAction(userId: int, currentLoginTimestamp:int, currentLoginResul
raise SDGBApiError("邪门错误")
# 成功上传后退出循环
break
else: # 重试次数超过3次
else: # 重试次数超过3次
raise SDGBRequestError
logger.info("上机:结果:"+ str(currentUserAllResult))
logger.info("上机:结果:" + str(currentUserAllResult))
return currentUserAllResult

View File

@@ -1,59 +1,62 @@
# 获取用户成绩的各种实现
from API_TitleServer import *
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from Config import *
import rapidjson as json
from HelperMusicDB import getMusicTitle
from loguru import logger
import sys
from HelperMusicDB import getMusicTitle
from API_TitleServer import apiSDGB
from MyConfig import testUid
# 日志设置
#log_level = "DEBUG"
#log_format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS zz}</green> | <level>{level: <8}</level> | <yellow>Line {line: >4} ({file}):</yellow> <b>{message}</b>"
#logger.add(sys.stderr, level=log_level, format=log_format, colorize=True, backtrace=True, diagnose=True)
#logger.add("file.log", level=log_level, format=log_format, colorize=False, backtrace=True, diagnose=True)
# log_level = "DEBUG"
# log_format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS zz}</green> | <level>{level: <8}</level> | <yellow>Line {line: >4} ({file}):</yellow> <b>{message}</b>"
# logger.add(sys.stderr, level=log_level, format=log_format, colorize=True, backtrace=True, diagnose=True)
# logger.add("file.log", level=log_level, format=log_format, colorize=False, backtrace=True, diagnose=True)
def getUserMusicDetail(userId:int, nextIndex:int=0, maxCount:int=50) -> dict:
def getUserMusicDetail(userId: int, nextIndex: int = 0, maxCount: int = 50) -> dict:
"""获取用户的成绩的API"""
data = json.dumps({
"userId": int(userId),
"nextIndex": nextIndex,
"maxCount": maxCount
})
data = json.dumps(
{"userId": int(userId), "nextIndex": nextIndex, "maxCount": maxCount}
)
return json.loads(apiSDGB(data, "GetUserMusicApi", userId))
def getUserFullMusicDetail(userId: int):
"""获取用户的全部成绩"""
currentUserMusicDetailList = []
nextIndex:int|None = None # 初始化 nextIndex
while nextIndex != 0 or nextIndex is None: #只要还有nextIndex就一直获取获取
nextIndex: int | None = None # 初始化 nextIndex
while nextIndex != 0 or nextIndex is None: # 只要还有nextIndex就一直获取获取
userMusicResponse = getUserMusicDetail(userId, nextIndex or 0)
nextIndex = userMusicResponse['nextIndex']
nextIndex = userMusicResponse["nextIndex"]
logger.info(f"NextIndex: {nextIndex}")
# 处理已经没有 userMusicList 的情况
if not userMusicResponse['userMusicList']:
if not userMusicResponse["userMusicList"]:
break
# 只要还有 userMusicList 就一直加进去,直到全部获取完毕
for currentMusic in userMusicResponse['userMusicList']:
for currentMusicDetail in currentMusic['userMusicDetailList']:
if not currentMusicDetail['playCount'] > 0:
for currentMusic in userMusicResponse["userMusicList"]:
for currentMusicDetail in currentMusic["userMusicDetailList"]:
if not currentMusicDetail["playCount"] > 0:
continue
currentUserMusicDetailList.append(currentMusicDetail)
return currentUserMusicDetailList
def parseUserFullMusicDetail(userFullMusicDetailList: list):
"""解析用户的全部成绩,给出一个迫真人类可读 list 套 dict"""
musicDetailList = []
for currentMusicDetail in userFullMusicDetailList:
musicDetailList.append({
'歌名': getMusicTitle(currentMusicDetail['musicId']),
'难度': currentMusicDetail['level'],
'分数': currentMusicDetail['achievement'] / 10000,
'DX分数': currentMusicDetail['deluxscoreMax']
})
musicDetailList.append(
{
"歌名": getMusicTitle(currentMusicDetail["musicId"]),
"难度": currentMusicDetail["level"],
"分数": currentMusicDetail["achievement"] / 10000,
"DX分数": currentMusicDetail["deluxscoreMax"],
}
)
return musicDetailList
if __name__ == '__main__':
if __name__ == "__main__":
userId = testUid
userFullMusicDetailList = getUserFullMusicDetail(userId)
parsedUserFullMusicDetail = parseUserFullMusicDetail(userFullMusicDetailList)

View File

@@ -1,9 +1,9 @@
# 获取用户数据的 API 实现
from loguru import logger
import rapidjson as json
from API_TitleServer import apiSDGB
def implGetUser_(thing:str, userId:int, noLog=False) -> dict:
def implGetUser_(thing: str, userId: int, noLog=False) -> dict:
"""获取用户某些数据的 API 实现,返回 Dict"""
# 获取 Json String
result = apiGetUserThing(userId, thing, noLog)
@@ -12,14 +12,12 @@ def implGetUser_(thing:str, userId:int, noLog=False) -> dict:
# 返回 Dict
return userthingDict
def apiGetUserThing(userId:int, thing:str, noLog=False) -> str:
def apiGetUserThing(userId: int, thing: str, noLog=False) -> str:
"""获取用户数据的 API 请求器,返回 Json String"""
# 构建 Payload
data = json.dumps({
"userId": userId
})
data = json.dumps({"userId": userId})
# 发送请求
userthing_result = apiSDGB(data, "GetUser" + thing + "Api", userId, noLog)
# 返回响应
return userthing_result

View File

@@ -1,62 +1,83 @@
# 登录·登出实现
# 一般作为模块使用,但也可以作为 CLI 程序运行以强制登出账号。
import rapidjson as json
import time
from loguru import logger
import random
from Config import *
from API_TitleServer import apiSDGB
import rapidjson as json
from loguru import logger
def apiLogin(timestamp:int, userId:int, noLog:bool=False) -> dict:
from API_TitleServer import apiSDGB
from Config import (
clientId,
placeId,
regionId,
)
from MyConfig import testUid
def apiLogin(timestamp: int, userId: int, noLog: bool = False) -> dict:
"""登录,返回 dict"""
data = json.dumps({
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"dateTime": timestamp,
"isContinue": False,
"genericFlag": 0,
})
data = json.dumps(
{
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"dateTime": timestamp,
"isContinue": False,
"genericFlag": 0,
}
)
login_result = json.loads(apiSDGB(data, "UserLoginApi", userId, noLog))
if not noLog:
logger.info("登录:结果:"+ str(login_result))
logger.info("登录:结果:" + str(login_result))
return login_result
def apiLogout(timestamp:int, userId:int, noLog:bool=False) -> dict:
def apiLogout(timestamp: int, userId: int, noLog: bool = False) -> dict:
"""登出,返回 dict"""
data = json.dumps({
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"dateTime": timestamp,
"type": 1
})
data = json.dumps(
{
"userId": userId,
"accessCode": "",
"regionId": regionId,
"placeId": placeId,
"clientId": clientId,
"dateTime": timestamp,
"type": 1,
}
)
logout_result = json.loads(apiSDGB(data, "UserLogoutApi", userId, noLog))
if not noLog:
logger.info("登出:结果:"+ str(logout_result))
logger.info("登出:结果:" + str(logout_result))
return logout_result
def generateTimestampLegacy() -> int:
"""生成一个凑合用的时间戳"""
timestamp = int(time.time()) - 60
logger.info(f"生成时间戳: {timestamp}")
return timestamp
def generateTimestamp() -> int:
"""生成一个今天早上 10:00 随机偏移的时间戳"""
timestamp = int(time.mktime(time.strptime(time.strftime("%Y-%m-%d 10:00:00"), "%Y-%m-%d %H:%M:%S"))) + random.randint(-600, 600)
timestamp = int(
time.mktime(
time.strptime(time.strftime("%Y-%m-%d 10:00:00"), "%Y-%m-%d %H:%M:%S")
)
) + random.randint(-600, 600)
logger.info(f"生成时间戳: {timestamp}")
logger.info(f"此时间戳对应的时间为: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}")
logger.info(
f"此时间戳对应的时间为: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}"
)
return timestamp
if __name__ == "__main__":
print("强制登出 CLI")
uid = testUid
timestamp = input("Timestamp: ")
timestamp = generateTimestamp()
apiLogout(int(timestamp), int(uid))

View File

@@ -4,9 +4,10 @@ import rapidjson as json
from loguru import logger
from HelperGetUserThing import implGetUser_
import unicodedata
from Config import *
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from MyConfig import testUid
def numberToLetter(number):
"""
@@ -17,10 +18,11 @@ def numberToLetter(number):
else:
return None
def maimaiVersionToHumanReadable(romVersion: str, dataVersion: str) -> str:
try:
romVersionList = romVersion.split('.')
dataVersionList = dataVersion.split('.')
romVersionList = romVersion.split(".")
dataVersionList = dataVersion.split(".")
except Exception as e:
logger.warning(f"无法解析版本号: {romVersion} {dataVersion},错误:{e}")
return "无效版本号:无法解析"
@@ -52,24 +54,19 @@ def maimaiVersionToHumanReadable(romVersion: str, dataVersion: str) -> str:
finalVersionString = f"{versionStringPrefix}{finalVersionList[0]}.{finalVersionList[1]}{finalVersionList[2]}"
return finalVersionString
levelIdDict = {
"绿": 0,
"": 1,
"": 2,
"": 3,
"": 4,
"": 5
}
levelIdDict = {"绿": 0, "": 1, "": 2, "": 3, "": 4, "": 5}
def getHalfWidthString(s):
"""全角转半角舞萌ID用"""
return unicodedata.normalize('NFKC', s)
return unicodedata.normalize("NFKC", s)
def getHumanReadableLoginErrorCode(loginResult) -> str:
'''解析登录结果并且给出中文的报错解释'''
match loginResult['returnCode']:
"""解析登录结果并且给出中文的报错解释"""
match loginResult["returnCode"]:
case 1:
return False
case 100:
@@ -79,10 +76,11 @@ def getHumanReadableLoginErrorCode(loginResult) -> str:
case 103:
return "❌ 试图登录的账号 UID 无效,请检查账号是否正确。"
case _:
return "❌ 登录失败!这不应该发生,请反馈此问题。错误详情:"+ loginResult
return "❌ 登录失败!这不应该发生,请反馈此问题。错误详情:" + loginResult
def checkTechnologyUseCount(userId: int) -> int:
'''猜测账号是否用了科技0没用过其他为用过'''
"""猜测账号是否用了科技0没用过其他为用过"""
userData1 = implGetUser_("Data", userId)
userData = userData1.get("userData", {})
userRegion = implGetUser_("Region", userId)
@@ -92,13 +90,16 @@ def checkTechnologyUseCount(userId: int) -> int:
allRegionPlayCount = 0
for region in userRegionList:
allRegionPlayCount += region.get("playCount", 0)
logger.info(f"用户 {userId} 的总游玩次数: {playCount}, 各地区游玩次数: {allRegionPlayCount}")
logger.info(
f"用户 {userId} 的总游玩次数: {playCount}, 各地区游玩次数: {allRegionPlayCount}"
)
# 计算全部的 Region 加起来的游玩次数是否和 playCount 对不上,对不上就是用了科技
# 返回差值
return playCount - allRegionPlayCount
def getFriendlyUserData(userId:int) -> str:
'''生成一个(相对)友好的UserData的人话'''
def getFriendlyUserData(userId: int) -> str:
"""生成一个(相对)友好的UserData的人话"""
userData1 = implGetUser_("Data", userId)
userData = userData1.get("userData", {})
userRegion = implGetUser_("Region", userId)
@@ -114,7 +115,7 @@ def getFriendlyUserData(userId:int) -> str:
result += f"最近登录版本: {maimaiVersionToHumanReadable(userData.get('lastRomVersion'), userData.get('lastDataVersion'))} "
result += f"最近登录地区: {userData.get('lastRegionName', '未知')}\n"
result += f"注册日期: {userData.get('firstPlayDate')} "
result += f"注册版本: {maimaiVersionToHumanReadable(userData.get('firstRomVersion'),userData.get('firstDataVersion'))}\n"
result += f"注册版本: {maimaiVersionToHumanReadable(userData.get('firstRomVersion'), userData.get('firstDataVersion'))}\n"
result += f"封号状态(banState): {banState}\n"
try:
logger.info(userRegion)
@@ -124,28 +125,31 @@ def getFriendlyUserData(userId:int) -> str:
return result
def getHumanReadableRegionData(userRegion:str) -> str:
'''生成一个人类可读的地区数据'''
def getHumanReadableRegionData(userRegion: str) -> str:
"""生成一个人类可读的地区数据"""
userRegionList = userRegion.get("userRegionList")
logger.info(userRegionList)
result = ""
for region in userRegionList:
regionName = WAHLAP_REGIONS.get(region['regionId'], '未知')
playCount = region['playCount']
created = region['created']
regionName = WAHLAP_REGIONS.get(region["regionId"], "未知")
playCount = region["playCount"]
created = region["created"]
result += f"\n{regionName} 游玩次数: {playCount} 首次游玩: {created}"
return result
def getHumanReadablePreview(preview_json_content:str) -> str:
'''简单,粗略地解释 Preview 的 Json String 为人话。'''
def getHumanReadablePreview(preview_json_content: str) -> str:
"""简单,粗略地解释 Preview 的 Json String 为人话。"""
previewData = json.loads(preview_json_content)
userName = getHalfWidthString(previewData['userName'])
playerRating = previewData['playerRating']
userName = getHalfWidthString(previewData["userName"])
playerRating = previewData["playerRating"]
finalString = f"用户名:{userName}\nDX RATING{playerRating}\n"
return finalString
def getHumanReadableLoginBonusList(jsonString: str):
'''生成一个人类可读的 Login Bonus 的列表'''
"""生成一个人类可读的 Login Bonus 的列表"""
data = json.loads(jsonString)
result = []
@@ -157,32 +161,34 @@ def getHumanReadableLoginBonusList(jsonString: str):
result.append(line)
resultString = ""
for line in result: # 转成字符串
for line in result: # 转成字符串
resultString += line + "\n"
return resultString
def getHumanReadableTicketList(jsonString: str):
'''生成一个人类可读的 UserCharge 的列表'''
"""生成一个人类可读的 UserCharge 的列表"""
data = json.loads(jsonString)
userId = data['userId']
length = data['length']
userChargeList = data['userChargeList']
userId = data["userId"]
length = data["length"]
userChargeList = data["userChargeList"]
result = f"UID: {userId} 票槽大小: {length} 所有记录:"
for currentItem in userChargeList:
chargeId = currentItem['chargeId']
stock = currentItem['stock']
purchaseDate = currentItem['purchaseDate']
validDate = currentItem['validDate']
chargeId = currentItem["chargeId"]
stock = currentItem["stock"]
purchaseDate = currentItem["purchaseDate"]
validDate = currentItem["validDate"]
result += f"\nID: {chargeId} 持有: {stock}, 购买日期: {purchaseDate}, 有效期限: {validDate}"
return result
def getHumanReadableUserData(userData) -> str:
'''生成一个人类可读的 UserData 的数据(比较详细)'''
"""生成一个人类可读的 UserData 的数据(比较详细)"""
userId = userData.get("userId")
userData = userData.get("userData", {})
banState = userData.get("banState")
@@ -239,39 +245,40 @@ def getHumanReadableUserData(userData) -> str:
result += f"封号状态: {banState}\n"
return result
WAHLAP_REGIONS = {
1: '北京',
2: '重庆',
3: '上海',
4: '天津',
5: '安徽',
6: '福建',
7: '甘肃',
8: '广东',
9: '贵州',
10: '海南',
11: '河北',
12: '黑龙江',
13: '河南',
14: '湖北',
15: '湖南',
16: '江苏',
17: '江西',
18: '吉林',
19: '辽宁',
20: '青海',
21: '陕西',
22: '山东',
23: '山西',
24: '四川',
25: '未知25',
26: '云南',
27: '浙江',
28: '广西',
29: '内蒙古',
30: '宁夏',
31: '新疆',
32: '西藏',
1: "北京",
2: "重庆",
3: "上海",
4: "天津",
5: "安徽",
6: "福建",
7: "甘肃",
8: "广东",
9: "贵州",
10: "海南",
11: "河北",
12: "黑龙江",
13: "河南",
14: "湖北",
15: "湖南",
16: "江苏",
17: "江西",
18: "吉林",
19: "辽宁",
20: "青海",
21: "陕西",
22: "山东",
23: "山西",
24: "四川",
25: "未知25",
26: "云南",
27: "浙江",
28: "广西",
29: "内蒙古",
30: "宁夏",
31: "新疆",
32: "西藏",
}
if __name__ == "__main__":
@@ -284,12 +291,12 @@ if __name__ == "__main__":
currentLoginTimestamp = generateTimestamp()
loginResult = apiLogin(currentLoginTimestamp, userId)
if loginResult['returnCode'] != 1:
if loginResult["returnCode"] != 1:
logger.info("登录失败")
exit()
try:
logger.info(checkTechnologyUseCount(userId))
#logger.info(apiQueryTicket(userId))
# logger.info(apiQueryTicket(userId))
finally:
logger.info(apiLogout(currentLoginTimestamp, userId))
#logger.warning("Error")
# logger.warning("Error")

View File

@@ -4,11 +4,11 @@ from loguru import logger
def getMusicTitle(musicId: int) -> str:
"""从数据库获取音乐的标题"""
#logger.debug(f"查询歌名: {musicId}")
# logger.debug(f"查询歌名: {musicId}")
musicInfo = musicDB.get(musicId)
if not musicInfo:
logger.warning(f"数据库里未找到此歌曲: {musicId}")
return "R_ERR_MUSIC_ID_NOT_IN_DATABASE"
musicName = musicInfo.get("name")
#logger.debug(f"成功查询到歌名: {musicName}")
return musicName
# logger.debug(f"成功查询到歌名: {musicName}")
return musicName

View File

@@ -1,43 +1,48 @@
# 解锁东西的一个通用的助手,不可独立使用
from loguru import logger
from Config import *
from HelperFullPlay import implFullPlayAction
def implUnlockThing(newUserItemList, userId: int, currentLoginTimestamp:int, currentLoginResult) -> str:
musicData= ({
"musicId": 11538, # Amber Chronicle
"level": 0,
"playCount": 1,
"achievement": 0,
"comboStatus": 0,
"syncStatus": 0,
"deluxscoreMax": 0,
"scoreRank": 0,
"extNum1": 0
})
def implUnlockThing(
newUserItemList, userId: int, currentLoginTimestamp: int, currentLoginResult
) -> str:
musicData = {
"musicId": 11538, # Amber Chronicle
"level": 0,
"playCount": 1,
"achievement": 0,
"comboStatus": 0,
"syncStatus": 0,
"deluxscoreMax": 0,
"scoreRank": 0,
"extNum1": 0,
}
userAllPatches = {
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1",
"userItemList": newUserItemList,
"isNewItemList": "1" * len(newUserItemList)
}}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
"upsertUserAll": {
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1",
"userItemList": newUserItemList,
"isNewItemList": "1" * len(newUserItemList),
}
}
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
itemKindDict = {
"PLATE": 1, # 姓名框
"TITLE": 2, # 称号
"ICON": 3, # 头像
"MUSIC": 5, # 歌
"MUSIC_MASTER": 6, # 紫谱
"MUSIC_RE_MASTER": 7,# 白谱
"CHARACTER": 9, # 旅行伙伴
"PARTNER": 10, # 搭档
"FRAME": 11, # 背景板
"TICKET": 12 # 功能票
# "PRESENT": 4, #
# "MUSIC_STRONG": 8, #
"PLATE": 1, # 姓名框
"TITLE": 2, # 称号
"ICON": 3, # 头像
"MUSIC": 5, # 歌
"MUSIC_MASTER": 6, # 紫谱
"MUSIC_RE_MASTER": 7, # 白谱
"CHARACTER": 9, # 旅行伙伴
"PARTNER": 10, # 搭档
"FRAME": 11, # 背景板
"TICKET": 12, # 功能票
# "PRESENT": 4, #
# "MUSIC_STRONG": 8, #
}
itemKindzhCNDict = {
@@ -50,9 +55,9 @@ itemKindzhCNDict = {
"旅行伙伴": "CHARACTER",
"搭档": "PARTNER",
"背景板": "FRAME",
"功能票": "TICKET"
# "礼物": "PRESENT",
# "STRONG": "MUSIC_STRONG",
"功能票": "TICKET",
# "礼物": "PRESENT",
# "STRONG": "MUSIC_STRONG",
}
partnerList = {
@@ -75,5 +80,5 @@ partnerList = {
"26": "黒姫",
"27": "俊达萌",
"28": "乙姫2024",
"29": "青柠熊柠檬熊2024"
}
"29": "青柠熊柠檬熊2024",
}

View File

@@ -8,133 +8,146 @@ from datetime import datetime
from loguru import logger
from API_TitleServer import apiSDGB
from Config import *
from Config import (
placeId,
placeName,
)
def apiUploadUserPlaylog(userId:int, musicDataToBeUploaded, currentUserData2, loginId:int) -> str:
def apiUploadUserPlaylog(
userId: int, musicDataToBeUploaded, currentUserData2, loginId: int
) -> str:
"""
上传一个 UserPlayLog。
注意:成绩为随机的空成绩,只用作占位
返回 Json String。"""
# 构建一个 PlayLog
data = json.dumps({
"userId": int(userId),
"userPlaylogList": [
data = json.dumps(
{
"userId": 0,
"orderId": 0,
"playlogId": loginId,
"version": 1051000,
"placeId": placeId,
"placeName": placeName,
"loginDate": int(time.time()), #似乎和登录timestamp不同
"playDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d'),
"userPlayDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') + '.0',
"type": 0,
"musicId": int(musicDataToBeUploaded['musicId']),
"level": int(musicDataToBeUploaded['level']),
"trackNo": 1,
"vsMode": 0,
"vsUserName": "",
"vsStatus": 0,
"vsUserRating": 0,
"vsUserAchievement": 0,
"vsUserGradeRank": 0,
"vsRank": 0,
"playerNum": 1,
"playedUserId1": 0,
"playedUserName1": "",
"playedMusicLevel1": 0,
"playedUserId2": 0,
"playedUserName2": "",
"playedMusicLevel2": 0,
"playedUserId3": 0,
"playedUserName3": "",
"playedMusicLevel3": 0,
"characterId1": currentUserData2['charaSlot'][0],
"characterLevel1": random.randint(1000,6500),
"characterAwakening1": 5,
"characterId2": currentUserData2['charaSlot'][1],
"characterLevel2": random.randint(1000,6500),
"characterAwakening2": 5,
"characterId3": currentUserData2['charaSlot'][2],
"characterLevel3": random.randint(1000,6500),
"characterAwakening3": 5,
"characterId4": currentUserData2['charaSlot'][3],
"characterLevel4": random.randint(1000,6500),
"characterAwakening4": 5,
"characterId5": currentUserData2['charaSlot'][4],
"characterLevel5": random.randint(1000,6500),
"characterAwakening5": 5,
"achievement": int(musicDataToBeUploaded['achievement']),
"deluxscore": int(musicDataToBeUploaded['deluxscoreMax']),
"scoreRank": int(musicDataToBeUploaded['scoreRank']),
"maxCombo": 0,
"totalCombo": random.randint(700,900),
"maxSync": 0,
"totalSync": 0,
"tapCriticalPerfect": 0,
"tapPerfect": 0,
"tapGreat": 0,
"tapGood": 0,
"tapMiss": random.randint(1,10),
"holdCriticalPerfect": 0,
"holdPerfect": 0,
"holdGreat": 0,
"holdGood": 0,
"holdMiss": random.randint(1,15),
"slideCriticalPerfect": 0,
"slidePerfect": 0,
"slideGreat": 0,
"slideGood": 0,
"slideMiss": random.randint(1,15),
"touchCriticalPerfect": 0,
"touchPerfect": 0,
"touchGreat": 0,
"touchGood": 0,
"touchMiss": random.randint(1,15),
"breakCriticalPerfect": 0,
"breakPerfect": 0,
"breakGreat": 0,
"breakGood": 0,
"breakMiss": random.randint(1,15),
"isTap": True,
"isHold": True,
"isSlide": True,
"isTouch": True,
"isBreak": True,
"isCriticalDisp": True,
"isFastLateDisp": True,
"fastCount": 0,
"lateCount": 0,
"isAchieveNewRecord": True,
"isDeluxscoreNewRecord": True,
"comboStatus": 0,
"syncStatus": 0,
"isClear": False,
"beforeRating": currentUserData2['playerRating'],
"afterRating": currentUserData2['playerRating'],
"beforeGrade": 0,
"afterGrade": 0,
"afterGradeRank": 1,
"beforeDeluxRating": currentUserData2['playerRating'],
"afterDeluxRating": currentUserData2['playerRating'],
"isPlayTutorial": False,
"isEventMode": False,
"isFreedomMode": False,
"playMode": 0,
"isNewFree": False,
"trialPlayAchievement": -1,
"extNum1": 0,
"extNum2": 0,
"extNum4": 3020,
"extBool1": False,
"extBool2": False
"userId": int(userId),
"userPlaylogList": [
{
"userId": 0,
"orderId": 0,
"playlogId": loginId,
"version": 1051000,
"placeId": placeId,
"placeName": placeName,
"loginDate": int(time.time()), # 似乎和登录timestamp不同
"playDate": datetime.now(pytz.timezone("Asia/Shanghai")).strftime(
"%Y-%m-%d"
),
"userPlayDate": datetime.now(
pytz.timezone("Asia/Shanghai")
).strftime("%Y-%m-%d %H:%M:%S")
+ ".0",
"type": 0,
"musicId": int(musicDataToBeUploaded["musicId"]),
"level": int(musicDataToBeUploaded["level"]),
"trackNo": 1,
"vsMode": 0,
"vsUserName": "",
"vsStatus": 0,
"vsUserRating": 0,
"vsUserAchievement": 0,
"vsUserGradeRank": 0,
"vsRank": 0,
"playerNum": 1,
"playedUserId1": 0,
"playedUserName1": "",
"playedMusicLevel1": 0,
"playedUserId2": 0,
"playedUserName2": "",
"playedMusicLevel2": 0,
"playedUserId3": 0,
"playedUserName3": "",
"playedMusicLevel3": 0,
"characterId1": currentUserData2["charaSlot"][0],
"characterLevel1": random.randint(1000, 6500),
"characterAwakening1": 5,
"characterId2": currentUserData2["charaSlot"][1],
"characterLevel2": random.randint(1000, 6500),
"characterAwakening2": 5,
"characterId3": currentUserData2["charaSlot"][2],
"characterLevel3": random.randint(1000, 6500),
"characterAwakening3": 5,
"characterId4": currentUserData2["charaSlot"][3],
"characterLevel4": random.randint(1000, 6500),
"characterAwakening4": 5,
"characterId5": currentUserData2["charaSlot"][4],
"characterLevel5": random.randint(1000, 6500),
"characterAwakening5": 5,
"achievement": int(musicDataToBeUploaded["achievement"]),
"deluxscore": int(musicDataToBeUploaded["deluxscoreMax"]),
"scoreRank": int(musicDataToBeUploaded["scoreRank"]),
"maxCombo": 0,
"totalCombo": random.randint(700, 900),
"maxSync": 0,
"totalSync": 0,
"tapCriticalPerfect": 0,
"tapPerfect": 0,
"tapGreat": 0,
"tapGood": 0,
"tapMiss": random.randint(1, 10),
"holdCriticalPerfect": 0,
"holdPerfect": 0,
"holdGreat": 0,
"holdGood": 0,
"holdMiss": random.randint(1, 15),
"slideCriticalPerfect": 0,
"slidePerfect": 0,
"slideGreat": 0,
"slideGood": 0,
"slideMiss": random.randint(1, 15),
"touchCriticalPerfect": 0,
"touchPerfect": 0,
"touchGreat": 0,
"touchGood": 0,
"touchMiss": random.randint(1, 15),
"breakCriticalPerfect": 0,
"breakPerfect": 0,
"breakGreat": 0,
"breakGood": 0,
"breakMiss": random.randint(1, 15),
"isTap": True,
"isHold": True,
"isSlide": True,
"isTouch": True,
"isBreak": True,
"isCriticalDisp": True,
"isFastLateDisp": True,
"fastCount": 0,
"lateCount": 0,
"isAchieveNewRecord": True,
"isDeluxscoreNewRecord": True,
"comboStatus": 0,
"syncStatus": 0,
"isClear": False,
"beforeRating": currentUserData2["playerRating"],
"afterRating": currentUserData2["playerRating"],
"beforeGrade": 0,
"afterGrade": 0,
"afterGradeRank": 1,
"beforeDeluxRating": currentUserData2["playerRating"],
"afterDeluxRating": currentUserData2["playerRating"],
"isPlayTutorial": False,
"isEventMode": False,
"isFreedomMode": False,
"playMode": 0,
"isNewFree": False,
"trialPlayAchievement": -1,
"extNum1": 0,
"extNum2": 0,
"extNum4": 3020,
"extBool1": False,
"extBool2": False,
}
],
}
]
})
)
# 发送请求
result = apiSDGB(data, "UploadUserPlaylogListApi", userId)
logger.info("上传游玩记录:结果:"+ str(result))
logger.info("上传游玩记录:结果:" + str(result))
# 返回响应
return result

View File

@@ -2,12 +2,20 @@
import pytz
from datetime import datetime
from Config import *
from HelperGetUserThing import implGetUser_
from HelperGetUserMusicDetail import getUserMusicDetail
from loguru import logger
from HelperGetUserThing import implGetUser_
from HelperGetUserMusicDetail import getUserMusicDetail
from Config import (
clientId,
placeName,
placeId,
regionId,
regionName,
)
def isNewMusicType(userId, musicId, level) -> str:
"""判断这首 musicId 在 isNewMusicDetailList 应该填什么
0: Edit
@@ -16,21 +24,38 @@ def isNewMusicType(userId, musicId, level) -> str:
未完工,仅供测试
"""
userMusicDetailList = getUserMusicDetail(userId, musicId, 1)['userMusicList'][0]['userMusicDetailList']
userMusicDetailList = getUserMusicDetail(userId, musicId, 1)["userMusicList"][0][
"userMusicDetailList"
]
logger.info(userMusicDetailList)
try:
if userMusicDetailList[0]['musicId'] == musicId and userMusicDetailList[0]['level'] == level:
if (
userMusicDetailList[0]["musicId"] == musicId
and userMusicDetailList[0]["level"] == level
):
logger.info(f"We think {musicId} Level {level} should use EDIT.")
return "0"
except:
except Exception:
return "1"
def generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial):
def generateFullUserAll(
userId,
currentLoginResult,
currentLoginTimestamp,
currentUserData2,
currentPlaySpecial,
):
"""从服务器取得必要的数据并构建一个比较完整的 UserAll"""
# 先构建一个基础 UserAll
currentUserAll = generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial)
currentUserAll = generateUserAllData(
userId,
currentLoginResult,
currentLoginTimestamp,
currentUserData2,
currentPlaySpecial,
)
# 然后从服务器取得必要的数据
currentUserExtend = implGetUser_("Extend", userId, True)
@@ -41,61 +66,78 @@ def generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, curre
currentUserMissionData = implGetUser_("MissionData", userId, True)
# 把这些数据都追加进去
currentUserAll['upsertUserAll']['userExtend'] = [currentUserExtend['userExtend']]
currentUserAll['upsertUserAll']['userOption'] = [currentUserOption['userOption']]
currentUserAll['upsertUserAll']['userRatingList'] = [currentUserRating['userRating']]
currentUserAll['upsertUserAll']['userActivityList'] = [currentUserActivity['userActivity']]
currentUserAll['upsertUserAll']['userChargeList'] = currentUserCharge['userChargeList']
currentUserAll['upsertUserAll']['userWeeklyData'] = currentUserMissionData['userWeeklyData']
currentUserAll["upsertUserAll"]["userExtend"] = [currentUserExtend["userExtend"]]
currentUserAll["upsertUserAll"]["userOption"] = [currentUserOption["userOption"]]
currentUserAll["upsertUserAll"]["userRatingList"] = [
currentUserRating["userRating"]
]
currentUserAll["upsertUserAll"]["userActivityList"] = [
currentUserActivity["userActivity"]
]
currentUserAll["upsertUserAll"]["userChargeList"] = currentUserCharge[
"userChargeList"
]
currentUserAll["upsertUserAll"]["userWeeklyData"] = currentUserMissionData[
"userWeeklyData"
]
# 完事
return currentUserAll
def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial):
def generateUserAllData(
userId,
currentLoginResult,
currentLoginTimestamp,
currentUserData2,
currentPlaySpecial,
):
"""构建一个非常基础的 UserAll 数据,必须手动填充一些数据"""
data = {
"userId": userId,
"playlogId": currentLoginResult['loginId'],
"playlogId": currentLoginResult["loginId"],
"isEventMode": False,
"isFreePlay": False,
"upsertUserAll": {
"userData": [
{
"accessCode": "",
"userName": currentUserData2['userName'],
"userName": currentUserData2["userName"],
"isNetMember": 1,
"point": currentUserData2['point'],
"totalPoint": currentUserData2['totalPoint'],
"iconId": currentUserData2['iconId'],
"plateId": currentUserData2['plateId'],
"titleId": currentUserData2['titleId'],
"partnerId": currentUserData2['partnerId'],
"frameId": currentUserData2['frameId'],
"selectMapId": currentUserData2['selectMapId'],
"totalAwake": currentUserData2['totalAwake'],
"gradeRating": currentUserData2['gradeRating'],
"musicRating": currentUserData2['musicRating'],
"playerRating": currentUserData2['playerRating'],
"highestRating": currentUserData2['highestRating'],
"gradeRank": currentUserData2['gradeRank'],
"classRank": currentUserData2['classRank'],
"courseRank": currentUserData2['courseRank'],
"charaSlot": currentUserData2['charaSlot'],
"charaLockSlot": currentUserData2['charaLockSlot'],
"contentBit": currentUserData2['contentBit'],
"playCount": currentUserData2['playCount'],
"currentPlayCount": currentUserData2['currentPlayCount'],
"point": currentUserData2["point"],
"totalPoint": currentUserData2["totalPoint"],
"iconId": currentUserData2["iconId"],
"plateId": currentUserData2["plateId"],
"titleId": currentUserData2["titleId"],
"partnerId": currentUserData2["partnerId"],
"frameId": currentUserData2["frameId"],
"selectMapId": currentUserData2["selectMapId"],
"totalAwake": currentUserData2["totalAwake"],
"gradeRating": currentUserData2["gradeRating"],
"musicRating": currentUserData2["musicRating"],
"playerRating": currentUserData2["playerRating"],
"highestRating": currentUserData2["highestRating"],
"gradeRank": currentUserData2["gradeRank"],
"classRank": currentUserData2["classRank"],
"courseRank": currentUserData2["courseRank"],
"charaSlot": currentUserData2["charaSlot"],
"charaLockSlot": currentUserData2["charaLockSlot"],
"contentBit": currentUserData2["contentBit"],
"playCount": currentUserData2["playCount"],
"currentPlayCount": currentUserData2["currentPlayCount"],
"renameCredit": 0,
"mapStock": currentUserData2['mapStock'],
"eventWatchedDate": currentUserData2['eventWatchedDate'],
"mapStock": currentUserData2["mapStock"],
"eventWatchedDate": currentUserData2["eventWatchedDate"],
"lastGameId": "SDGB",
"lastRomVersion": currentUserData2['lastRomVersion'],
"lastDataVersion": currentUserData2['lastDataVersion'],
#"lastLoginDate": currentLoginResult['lastLoginDate'], # sb
"lastLoginDate": currentUserData2['lastLoginDate'], # 等待测试
"lastPlayDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') + '.0',
"lastRomVersion": currentUserData2["lastRomVersion"],
"lastDataVersion": currentUserData2["lastDataVersion"],
# "lastLoginDate": currentLoginResult['lastLoginDate'], # sb
"lastLoginDate": currentUserData2["lastLoginDate"], # 等待测试
"lastPlayDate": datetime.now(
pytz.timezone("Asia/Shanghai")
).strftime("%Y-%m-%d %H:%M:%S")
+ ".0",
"lastPlayCredit": 1,
"lastPlayMode": 0,
"lastPlaceId": placeId,
@@ -107,68 +149,83 @@ def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, curre
"lastCountryCode": "CHN",
"lastSelectEMoney": 0,
"lastSelectTicket": 0,
"lastSelectCourse": currentUserData2['lastSelectCourse'],
"lastSelectCourse": currentUserData2["lastSelectCourse"],
"lastCountCourse": 0,
"firstGameId": "SDGB",
"firstRomVersion": currentUserData2['firstRomVersion'],
"firstDataVersion": currentUserData2['firstDataVersion'],
"firstPlayDate": currentUserData2['firstPlayDate'],
"compatibleCmVersion": currentUserData2['compatibleCmVersion'],
"dailyBonusDate": currentUserData2['dailyBonusDate'],
"dailyCourseBonusDate": currentUserData2['dailyCourseBonusDate'],
"lastPairLoginDate": currentUserData2['lastPairLoginDate'],
"lastTrialPlayDate": currentUserData2['lastTrialPlayDate'],
"firstRomVersion": currentUserData2["firstRomVersion"],
"firstDataVersion": currentUserData2["firstDataVersion"],
"firstPlayDate": currentUserData2["firstPlayDate"],
"compatibleCmVersion": currentUserData2["compatibleCmVersion"],
"dailyBonusDate": currentUserData2["dailyBonusDate"],
"dailyCourseBonusDate": currentUserData2["dailyCourseBonusDate"],
"lastPairLoginDate": currentUserData2["lastPairLoginDate"],
"lastTrialPlayDate": currentUserData2["lastTrialPlayDate"],
"playVsCount": 0,
"playSyncCount": 0,
"winCount": 0,
"helpCount": 0,
"comboCount": 0,
"totalDeluxscore": currentUserData2['totalDeluxscore'],
"totalBasicDeluxscore": currentUserData2['totalBasicDeluxscore'],
"totalAdvancedDeluxscore": currentUserData2['totalAdvancedDeluxscore'],
"totalExpertDeluxscore": currentUserData2['totalExpertDeluxscore'],
"totalMasterDeluxscore": currentUserData2['totalMasterDeluxscore'],
"totalReMasterDeluxscore": currentUserData2['totalReMasterDeluxscore'],
"totalSync": currentUserData2['totalSync'],
"totalBasicSync": currentUserData2['totalBasicSync'],
"totalAdvancedSync": currentUserData2['totalAdvancedSync'],
"totalExpertSync": currentUserData2['totalExpertSync'],
"totalMasterSync": currentUserData2['totalMasterSync'],
"totalReMasterSync": currentUserData2['totalReMasterSync'],
"totalAchievement": currentUserData2['totalAchievement'],
"totalBasicAchievement": currentUserData2['totalBasicAchievement'],
"totalAdvancedAchievement": currentUserData2['totalAdvancedAchievement'],
"totalExpertAchievement": currentUserData2['totalExpertAchievement'],
"totalMasterAchievement": currentUserData2['totalMasterAchievement'],
"totalReMasterAchievement": currentUserData2['totalReMasterAchievement'],
"playerOldRating": currentUserData2['playerOldRating'],
"playerNewRating": currentUserData2['playerNewRating'],
"totalDeluxscore": currentUserData2["totalDeluxscore"],
"totalBasicDeluxscore": currentUserData2["totalBasicDeluxscore"],
"totalAdvancedDeluxscore": currentUserData2[
"totalAdvancedDeluxscore"
],
"totalExpertDeluxscore": currentUserData2["totalExpertDeluxscore"],
"totalMasterDeluxscore": currentUserData2["totalMasterDeluxscore"],
"totalReMasterDeluxscore": currentUserData2[
"totalReMasterDeluxscore"
],
"totalSync": currentUserData2["totalSync"],
"totalBasicSync": currentUserData2["totalBasicSync"],
"totalAdvancedSync": currentUserData2["totalAdvancedSync"],
"totalExpertSync": currentUserData2["totalExpertSync"],
"totalMasterSync": currentUserData2["totalMasterSync"],
"totalReMasterSync": currentUserData2["totalReMasterSync"],
"totalAchievement": currentUserData2["totalAchievement"],
"totalBasicAchievement": currentUserData2["totalBasicAchievement"],
"totalAdvancedAchievement": currentUserData2[
"totalAdvancedAchievement"
],
"totalExpertAchievement": currentUserData2[
"totalExpertAchievement"
],
"totalMasterAchievement": currentUserData2[
"totalMasterAchievement"
],
"totalReMasterAchievement": currentUserData2[
"totalReMasterAchievement"
],
"playerOldRating": currentUserData2["playerOldRating"],
"playerNewRating": currentUserData2["playerNewRating"],
"banState": 0,
"friendRegistSkip": currentUserData2['friendRegistSkip'],
"dateTime": currentLoginTimestamp
"friendRegistSkip": currentUserData2["friendRegistSkip"],
"dateTime": currentLoginTimestamp,
}
],
"userExtend": [], #需要填上
"userOption": [], #需要填上
"userExtend": [], # 需要填上
"userOption": [], # 需要填上
"userGhost": [],
"userCharacterList": [],
"userMapList": [],
"userLoginBonusList": [],
"userRatingList": [], #需要填上
"userItemList": [], #可选,但经常要填上
"userMusicDetailList": [],#需要填上
"userRatingList": [], # 需要填上
"userItemList": [], # 可选,但经常要填上
"userMusicDetailList": [], # 需要填上
"userCourseList": [],
"userFriendSeasonRankingList": [],
"userChargeList": [], #需要填上
"userChargeList": [], # 需要填上
"userFavoriteList": [],
"userActivityList": [], #需要填上
"userActivityList": [], # 需要填上
"userMissionDataList": [],
"userWeeklyData": [],#应该需要填上
"userWeeklyData": [], # 应该需要填上
"userGamePlaylogList": [
{
"playlogId": currentLoginResult['loginId'],
"playlogId": currentLoginResult["loginId"],
"version": "1.51.00",
"playDate": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') + '.0',
"playDate": datetime.now(pytz.timezone("Asia/Shanghai")).strftime(
"%Y-%m-%d %H:%M:%S"
)
+ ".0",
"playMode": 0,
"useTicketId": -1,
"playCredit": 1,
@@ -177,9 +234,9 @@ def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, curre
"isPlayTutorial": False,
"isEventMode": False,
"isNewFree": False,
"playCount": currentUserData2['playCount'],
"playCount": currentUserData2["playCount"],
"playSpecial": currentPlaySpecial,
"playOtherUserId": 0
"playOtherUserId": 0,
}
],
"user2pPlaylog": {
@@ -189,9 +246,9 @@ def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, curre
"userName2": "",
"regionId": 0,
"placeId": 0,
"user2pPlaylogDetailList": []
"user2pPlaylogDetailList": [],
},
"userIntimateList": [],
"userIntimateList": [],
"userShopItemStockList": [],
"userGetPointList": [],
"userTradeItemList": [],
@@ -201,13 +258,13 @@ def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, curre
"isNewMapList": "",
"isNewLoginBonusList": "",
"isNewItemList": "",
"isNewMusicDetailList": "", #可选但经常要填上
"isNewMusicDetailList": "", # 可选但经常要填上
"isNewCourseList": "0",
"isNewFavoriteList": "",
"isNewFriendSeasonRankingList": "",
"isNewUserIntimateList": "",
"isNewFavoritemusicList": "",
"isNewKaleidxScopeList": ""
}
}
"isNewKaleidxScopeList": "",
},
}
return data

View File

@@ -6,17 +6,17 @@ from typing import Dict, Union
MusicDBType = Dict[int, Dict[str, Union[int, str]]]
# 将 '__all__' 用于模块导出声明
__all__ = ['musicDB']
__all__ = ["musicDB"]
# 读取并解析 JSON 文件
try:
with open(musicDBPath, 'r', encoding='utf-8') as f:
with open(musicDBPath, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
try:
with open(musicDBPathFallback, 'r', encoding='utf-8') as f:
with open(musicDBPathFallback, "r", encoding="utf-8") as f:
data = json.load(f)
except:
except Exception:
raise FileNotFoundError("musicDB.json 文件不存在!")
# 将 JSON 数据转换为指定格式的字典

View File

@@ -53,7 +53,7 @@ def main_sdga():
print(str(decompressedData))
def main_sdgb():
def main_sdgb140():
# 填入你的想解密的数据的 base64 编码
base64_encoded_data = "eJyrTVvpuGwCR32OdodwtVXZ7/Ofmfhin7k/K61q3XNoad1rAPGwECU="
@@ -71,3 +71,4 @@ def main_sdgb():
if __name__ == "__main__":
main_sdga()
main_sdgb140()

View File

@@ -2,30 +2,40 @@ import sys
import rapidjson as json
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QLineEdit, QTextEdit, QPushButton, QLabel, QHBoxLayout
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QLineEdit,
QTextEdit,
QPushButton,
QLabel,
QHBoxLayout,
)
from PyQt6.QtCore import Qt
# 将当前目录的父目录加入到 sys.path 中
from pathlib import Path
current_dir = Path(__file__).resolve().parent
parent_dir = current_dir.parent
sys.path.append(str(parent_dir))
from API_TitleServer import *
def sendRequest(requestText:str, apiNameText:str, uid:int) -> str:
def sendRequest(requestText: str, apiNameText: str, uid: int) -> str:
try:
data = json.loads(requestText)
data = json.dumps(data)
except:
except Exception:
return "给出的输入不是有效的 JSON"
try:
result = apiSDGB(data, apiNameText, uid)
except Exception as e:
return "请求失败:" + str(e)
return result
class ApiTester(QMainWindow):
def __init__(self):
@@ -70,21 +80,18 @@ class ApiTester(QMainWindow):
self.ResponseTextBox.setReadOnly(True)
MainLayout.addWidget(self.ResponseTextBox)
# 布局设定
MainLayout.setContentsMargins(5, 5, 5, 5)
MainLayout.setSpacing(5)
MainLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
def prepareRequest(self):
# 发送请求用
try:
RequestDataString = self.RequestInputBox.toPlainText()
TargetAPIString = self.TargetAPIInputBox.text()
AgentExtraString = int(self.AgentExtraInputBox.text())
except:
except Exception:
self.ResponseTextBox.setPlainText("输入无效")
return
@@ -93,12 +100,13 @@ class ApiTester(QMainWindow):
# 显示出输出
self.ResponseTextBox.setPlainText(Result)
if __name__ == "__main__":
app = QApplication(sys.argv)
# Set proper style for each OS
#if sys.platform == "win32":
# if sys.platform == "win32":
# app.setStyle("windowsvista")
#else:
# else:
# app.setStyle("Fusion")
window = ApiTester()
window.show()

View File

@@ -1,36 +1,55 @@
# 纯纯测试用
from loguru import logger
from Config import *
from HelperLogInOut import apiLogin, apiLogout, generateTimestamp
from HelperFullPlay import implFullPlayAction, generateMusicData
def implChangeVersionNumber(userId: int, currentLoginTimestamp:int, currentLoginResult, dataVersion="1.40.09", romVersion="1.41.00") -> str:
from MyConfig import testUid8
def implChangeVersionNumber(
userId: int,
currentLoginTimestamp: int,
currentLoginResult,
dataVersion="1.40.09",
romVersion="1.41.00",
) -> str:
musicData = generateMusicData()
userAllPatches = {
"upsertUserAll": {
"userData": [{
"lastRomVersion": romVersion,
"lastDataVersion": dataVersion,
"playerRating": 114514
}],
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1" #1避免覆盖
}}
result = implFullPlayAction(userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches)
"upsertUserAll": {
"userData": [
{
"lastRomVersion": romVersion,
"lastDataVersion": dataVersion,
"playerRating": 114514,
}
],
"userMusicDetailList": [musicData],
"isNewMusicDetailList": "1", # 1避免覆盖
}
}
result = implFullPlayAction(
userId, currentLoginTimestamp, currentLoginResult, musicData, userAllPatches
)
return result
if __name__ == "__main__":
userId = testUid8
currentLoginTimestamp = generateTimestamp()
loginResult = apiLogin(currentLoginTimestamp, userId)
if loginResult['returnCode'] != 1:
if loginResult["returnCode"] != 1:
logger.info("登录失败")
exit()
try:
logger.info(implChangeVersionNumber(userId, currentLoginTimestamp, loginResult, "1.00.00", "1.00.00"))
logger.info(
implChangeVersionNumber(
userId, currentLoginTimestamp, loginResult, "1.00.00", "1.00.00"
)
)
logger.info(apiLogout(currentLoginTimestamp, userId))
finally:
logger.info(apiLogout(currentLoginTimestamp, userId))
#logger.warning("Error")
# logger.warning("Error")

15
pyproject.toml Normal file
View File

@@ -0,0 +1,15 @@
[project]
name = "maimaidx-api"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"datetime>=5.5",
"httpx>=0.28.1",
"loguru>=0.7.3",
"pycryptodome>=3.23.0",
"python-rapidjson>=1.21",
"pytz>=2025.2",
"requests>=2.32.4",
]

View File

@@ -1,7 +0,0 @@
pycryptodome
requests
httpx
python-rapidjson
pytz
loguru
datetime

317
uv.lock generated Normal file
View File

@@ -0,0 +1,317 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
[[package]]
name = "certifi"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "datetime"
version = "5.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytz" },
{ name = "zope-interface" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/66/e284b9978fede35185e5d18fb3ae855b8f573d8c90a56de5f6d03e8ef99e/DateTime-5.5.tar.gz", hash = "sha256:21ec6331f87a7fcb57bd7c59e8a68bfffe6fcbf5acdbbc7b356d6a9a020191d3", size = 63671, upload-time = "2024-03-21T07:26:50.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/78/8e382b8cb4346119e2e04270b6eb4a01c5ee70b47a8a0244ecdb157204f7/DateTime-5.5-py3-none-any.whl", hash = "sha256:0abf6c51cb4ba7cee775ca46ccc727f3afdde463be28dbbe8803631fefd4a120", size = 52649, upload-time = "2024-03-21T07:26:47.849Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
]
[[package]]
name = "maimaidx-api"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "datetime" },
{ name = "httpx" },
{ name = "loguru" },
{ name = "pycryptodome" },
{ name = "python-rapidjson" },
{ name = "pytz" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "datetime", specifier = ">=5.5" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
{ name = "python-rapidjson", specifier = ">=1.21" },
{ name = "pytz", specifier = ">=2025.2" },
{ name = "requests", specifier = ">=2.32.4" },
]
[[package]]
name = "pycryptodome"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
]
[[package]]
name = "python-rapidjson"
version = "1.21"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/da/f041c85e7852ddb87dd59e34fdbfebf27defc6e4dfecbc0486ab30553dfd/python_rapidjson-1.21.tar.gz", hash = "sha256:4d0dd9cf1fcb6f4bf79ee606e6e9be4cfa598f273b91338a6974b6a99309a1e6", size = 238903, upload-time = "2025-07-10T06:30:43.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/c8/52735f227a9c662df8e4292af2b13960f0351795b0115f9fe95b3bfcfe62/python_rapidjson-1.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:354ac34828a242eec1896901fabe59d14adab7a5054d09a8bdaed4c6ee510b22", size = 220850, upload-time = "2025-07-10T07:19:00.767Z" },
{ url = "https://files.pythonhosted.org/packages/c2/83/f02aa1c9ea2594f05dd8c6c51e2ecea398a462118c7f4ddc004a135096f0/python_rapidjson-1.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ddd846b095fad357e4e4c5071e874f9c100c2e5b9765abcc7d71254820251c20", size = 210311, upload-time = "2025-07-10T07:19:02.27Z" },
{ url = "https://files.pythonhosted.org/packages/2f/60/b6a4026fd144fe80db1d6f7cb78191ed2f27fb46c69c719b3b63a8542072/python_rapidjson-1.21-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7faf4211354c1db9b21934d723f3c7db1d611ec3013458dc2fe1a732aa100d3e", size = 1679351, upload-time = "2025-07-10T07:19:04.419Z" },
{ url = "https://files.pythonhosted.org/packages/35/7e/73ca21eb527fbe27b267ea04b9047fdfb4a524b2ce2565a082d84cdee7e9/python_rapidjson-1.21-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46ea95e9292260fd8174966eca05fad08b43ca0f5df1ccfab9796edb0868d8eb", size = 1722281, upload-time = "2025-07-10T07:19:06.637Z" },
{ url = "https://files.pythonhosted.org/packages/e1/9c/23da2d95b0b4de46695b57a593763ddb8e7f8a786c2793be89fc3dad957e/python_rapidjson-1.21-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1cac86399cfbaf24e72aef6eed1114180a72ba0242c7153f89d874a81fb83c6", size = 1719283, upload-time = "2025-07-10T07:19:07.958Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c1/52de9e4c137ccc98116c16835d9a708436d58ba51ba448e238ce08938d5b/python_rapidjson-1.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a4afbf54eb60dc56d4c2b6fac5d24709e8a5928baaeff0669c00950961c7a65", size = 2530062, upload-time = "2025-07-10T07:19:09.302Z" },
{ url = "https://files.pythonhosted.org/packages/7f/16/08d24136b5895157f1fdc75669796283d5116336c0b1c27fa73de260980f/python_rapidjson-1.21-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e8d4d1285c697325ad1b5300866176d190625bbdd85dce963300fad5bfaf166", size = 2635763, upload-time = "2025-07-10T07:19:10.764Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a3/08aecff00e26e5dbb8cc8e5b2372b6f9ada28fcf09623e6de62f08ed2b99/python_rapidjson-1.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1525778bc9b9cee9b8f3058e0435f0e4ff03e2d8c3883893ebcaf58680316964", size = 2635419, upload-time = "2025-07-10T07:19:12.791Z" },
{ url = "https://files.pythonhosted.org/packages/38/21/bd4e0cda3eada4d99562c1b5bcd2cb54588df17d63ab95b8b76af3d93826/python_rapidjson-1.21-cp312-cp312-win32.whl", hash = "sha256:b5717ddb9788ca00e0f97546cbdd3cfd0c25e52ab3bfed0951c7898e7f37cc60", size = 128528, upload-time = "2025-07-10T07:19:14.61Z" },
{ url = "https://files.pythonhosted.org/packages/e2/91/70f9083930de93e7ce9ccebd8bd598a5737829452c03c258ea862146bbfa/python_rapidjson-1.21-cp312-cp312-win_amd64.whl", hash = "sha256:409704e52ad25df661265dbcb3a512a2146c98f72f362adc157e9b595704e1be", size = 148421, upload-time = "2025-07-10T07:19:16.127Z" },
{ url = "https://files.pythonhosted.org/packages/6d/82/9f1ffd805d6c4b2329046f88b59eec077ea5b2a1894ec23f8fcf22f53e0f/python_rapidjson-1.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:863208f50b848b8e10aefcf0da2a67ce38a1044a2ebf83d6355f16f0c7865d4f", size = 220853, upload-time = "2025-07-10T07:19:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/21/d8/9de439a33cef3d1a7672028abcc1a3c8d0c9982f03a73b9cc71055325ee3/python_rapidjson-1.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:608ade2fff2788821f25e70af4b1bfbf9ab2b2dc8ad47c91616935c5d31e3b72", size = 210314, upload-time = "2025-07-10T07:19:19.292Z" },
{ url = "https://files.pythonhosted.org/packages/da/93/dbee2d13a1619747652b8c8a921cb8a2e7309c0a142867b4954341c76632/python_rapidjson-1.21-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf6377c4e9636e20b403776585903d1ff52613a419c5f8453032146f0422634", size = 1679359, upload-time = "2025-07-10T07:19:21.086Z" },
{ url = "https://files.pythonhosted.org/packages/a1/56/06535489e561f6c2a035a5fa8d3d2ecc97c647fb4e1625a8cb7a1db46e6b/python_rapidjson-1.21-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74cba817cd9b16a3a54e7ddb3b59980c338c9a28900124502215dcba5db5a276", size = 1722183, upload-time = "2025-07-10T07:19:23.025Z" },
{ url = "https://files.pythonhosted.org/packages/67/0b/7c27a2473edfb9d40eb65e5a225dc2e833a5e355f228502aa19c2bdf6679/python_rapidjson-1.21-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3194bde069c3f03f3c425de7037cca37d337b6c7ac30f42cd2f17e15f1c4da3a", size = 1718857, upload-time = "2025-07-10T07:19:24.369Z" },
{ url = "https://files.pythonhosted.org/packages/17/0a/0db35ac49212505c347cdc353ddebd2b4085bfc02090baae552e7b5908b4/python_rapidjson-1.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b663932dc7548567028fb735bf4092b50dc373822dc9756a9ade6513541013de", size = 2530213, upload-time = "2025-07-10T07:19:26.062Z" },
{ url = "https://files.pythonhosted.org/packages/02/6c/4972fb83f0419654d3083e0b4881f607cbf0829d64da43b7d2a3a21480a0/python_rapidjson-1.21-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fcb094ecca3d892f1dbd318251579ef66e2b2f06a2a24414ad71c31931d7ffff", size = 2636100, upload-time = "2025-07-10T07:19:27.744Z" },
{ url = "https://files.pythonhosted.org/packages/3e/08/da452b2bd67f88b8232e48f2341f3320ce9315f0bdfed1ca769b0b389acf/python_rapidjson-1.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f4cdb4f77b08b7ae121b7ca27ed2d57dcce17f8cd9f1d4593c954c982aba18ba", size = 2635274, upload-time = "2025-07-10T07:19:29.174Z" },
{ url = "https://files.pythonhosted.org/packages/18/2b/b81e89c01ec25078a2326051b691443c90797da0d21e1b6c862128de5238/python_rapidjson-1.21-cp313-cp313-win32.whl", hash = "sha256:be63c0ef87bf26059ee77f5de21d84a1be3659cb6baa8484c237ffe17a8beb56", size = 128530, upload-time = "2025-07-10T07:19:30.743Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/e92189d6142860f949b9679c915a6a3752ccc2345a5f80a3995233c45f9f/python_rapidjson-1.21-cp313-cp313-win_amd64.whl", hash = "sha256:38307a2ef4fddbdc18806a796201fe668c30e60de7cd795943a3f7fd39535c8b", size = 148424, upload-time = "2025-07-10T07:19:32.212Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
]
[[package]]
name = "zope-interface"
version = "7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960, upload-time = "2024-11-28T08:45:39.224Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959, upload-time = "2024-11-28T08:47:47.788Z" },
{ url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357, upload-time = "2024-11-28T08:47:50.897Z" },
{ url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235, upload-time = "2024-11-28T09:18:15.56Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253, upload-time = "2024-11-28T08:48:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702, upload-time = "2024-11-28T08:48:37.363Z" },
{ url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466, upload-time = "2024-11-28T08:49:14.397Z" },
{ url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961, upload-time = "2024-11-28T08:48:29.865Z" },
{ url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356, upload-time = "2024-11-28T08:48:33.297Z" },
{ url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196, upload-time = "2024-11-28T09:18:17.584Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237, upload-time = "2024-11-28T08:48:31.71Z" },
{ url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696, upload-time = "2024-11-28T08:48:41.161Z" },
{ url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472, upload-time = "2024-11-28T08:49:56.587Z" },
]