Skip to content

Commit be15fe7

Browse files
committed
将表单独存到一个文件
1 parent a1e8736 commit be15fe7

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

modules/self_contained/pica/pica.py

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import hmac
2+
import json
3+
import time
4+
import uuid
5+
import asyncio
6+
import aiohttp
7+
from yarl import URL
8+
from pathlib import Path
9+
from loguru import logger
10+
from hashlib import sha256
11+
from typing import Literal
12+
from aiohttp import TCPConnector, ClientSession
13+
14+
from creart import create
15+
16+
from shared.models.config import GlobalConfig
17+
18+
BASE_PATH = Path(__file__).parent
19+
CACHE_PATH = BASE_PATH / "cache" / "download"
20+
global_url = URL("https://picaapi.picacomic.com/")
21+
api_key = "C69BAF41DA5ABD1FFEDC6D2FEA56B"
22+
uuid_s = str(uuid.uuid4()).replace("-", "")
23+
header = {
24+
"api-key": "C69BAF41DA5ABD1FFEDC6D2FEA56B",
25+
"app-channel": "3",
26+
"app-version": "2.2.1.3.3.4",
27+
"app-uuid": "defaultUuid",
28+
"image-quality": "original",
29+
"app-platform": "android",
30+
"app-build-version": "45",
31+
"Content-Type": "application/json; charset=UTF-8",
32+
"User-Agent": "okhttp/3.8.1",
33+
"accept": "application/vnd.picacomic.com.v1+json",
34+
"time": 0,
35+
"nonce": "",
36+
"signature": "encrypt",
37+
}
38+
path_filter = ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]
39+
40+
config = create(GlobalConfig)
41+
loop = create(asyncio.AbstractEventLoop)
42+
proxy = config.proxy if config.proxy != "proxy" else ""
43+
pica_config = config.functions.get("pica", {})
44+
username = pica_config.get("username", None)
45+
password = pica_config.get("password", None)
46+
compress_password = pica_config.get("compress_password", "i_luv_sagiri")
47+
DOWNLOAD_CACHE = pica_config.get("download_cache", True)
48+
49+
50+
class Pica:
51+
def __init__(self, account, pwd):
52+
CACHE_PATH.mkdir(parents=True, exist_ok=True)
53+
self.init = False
54+
self.account = account
55+
self.password = pwd
56+
self.header = header.copy()
57+
self.header["nonce"] = uuid_s
58+
self.__SigFromNative = (
59+
"~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn"
60+
)
61+
asyncio.run_coroutine_threadsafe(self.check(), loop)
62+
63+
@logger.catch
64+
async def check(self) -> bool | None:
65+
try:
66+
await self.login()
67+
self.init = True
68+
return True
69+
except aiohttp.ClientConnectorError:
70+
logger.error("proxy配置可能错误或失效,请检查")
71+
except KeyError:
72+
logger.error("pica 账号密码可能错误,请检查")
73+
74+
def update_signature(self, url: str | URL, method: Literal["GET", "POST"]) -> dict:
75+
if isinstance(url, str):
76+
url = URL(url)
77+
ts = str(int(time.time()))
78+
temp_header = self.header.copy()
79+
temp_header["time"] = ts
80+
temp_header["signature"] = self.encrypt(url, ts, method)
81+
if method == "GET":
82+
temp_header.pop("Content-Type")
83+
return temp_header
84+
85+
def encrypt(self, url: URL, ts, method):
86+
datas = [
87+
global_url,
88+
url.path[1:],
89+
ts,
90+
uuid_s,
91+
method,
92+
"C69BAF41DA5ABD1FFEDC6D2FEA56B",
93+
"2.2.1.3.3.4",
94+
"45",
95+
]
96+
_src = self.__ConFromNative(datas)
97+
_key = self.__SigFromNative
98+
return Pica.HashKey(_src, _key)
99+
100+
@staticmethod
101+
def __ConFromNative(datas):
102+
return "".join(map(str, datas[1:6]))
103+
104+
@staticmethod
105+
def HashKey(src, key):
106+
app_secret = key.encode("utf-8") # 秘钥
107+
data = src.lower().encode("utf-8") # 数据
108+
return hmac.new(app_secret, data, digestmod=sha256).hexdigest()
109+
110+
async def request(
111+
self,
112+
url: str | URL,
113+
params: dict[str, str] | None = None,
114+
method: Literal["GET", "POST"] = "GET",
115+
):
116+
temp_header = self.update_signature(url, method)
117+
# print(temp_header)
118+
data = json.dumps(params) if params else None
119+
# print(data)
120+
async with aiohttp.ClientSession(connector=TCPConnector(ssl=False)) as session:
121+
async with session.request(method, url=url, headers=temp_header, proxy=proxy, data=data) as resp:
122+
ret_data = await resp.json()
123+
if not resp.ok:
124+
logger.warning(f"报错返回json:{ret_data}")
125+
# print(await resp.json())
126+
return await resp.json()
127+
128+
async def login(self):
129+
"""登录获取token"""
130+
url = global_url / "auth" / "sign-in"
131+
send = {"email": self.account, "password": self.password}
132+
ret = await self.request(url, send, "POST")
133+
self.header["authorization"] = ret["data"]["token"]
134+
135+
async def categories(self):
136+
"""获取所有目录"""
137+
url = global_url / "categories"
138+
return (await self.request(url))["data"]["categories"]
139+
140+
async def search(self, keyword: str):
141+
"""关键词搜索"""
142+
url = global_url / "comics" / "advanced-search" % {"page": 1}
143+
# print(url)
144+
param = {"categories": [], "keyword": keyword, "sort": "ua"}
145+
return [
146+
{"name": comic["title"], "id": comic["_id"]}
147+
for q in range(1, 3)
148+
for comic in (await self.request(url % {"q": q}, param, "POST"))["data"]["comics"]["docs"]
149+
if comic["likesCount"] > 200
150+
and comic["pagesCount"] / comic["epsCount"] < 60
151+
and comic["epsCount"] < 10
152+
]
153+
154+
async def random(self):
155+
"""随机本子"""
156+
url = global_url / "comics" / "random"
157+
return (await self.request(url))["data"]["comics"]
158+
159+
async def rank(self, tt: Literal["H24", "D7", "D30"] = "H24"):
160+
"""排行榜"""
161+
url = global_url / "comics" / "leaderboard" % {"ct": "VC", "tt": tt}
162+
return (await self.request(url))["data"]["comics"]
163+
164+
async def comic_info(self, book_id: str):
165+
"""漫画详情"""
166+
url = global_url / "comics" / book_id
167+
return (await self.request(url))["data"]["comic"]
168+
169+
async def download_image(self, url: str, path: str | Path | None = None) -> bytes:
170+
async with aiohttp.ClientSession(connector=TCPConnector(ssl=False)) as session:
171+
return await self.download_image_session(session, url, path)
172+
173+
async def download_image_session(
174+
self, session: ClientSession, url: str, path: str | Path | None = None
175+
):
176+
temp_header = self.update_signature(url, "GET")
177+
async with session.get(url=url, headers=temp_header, proxy=proxy) as resp:
178+
resp.raise_for_status()
179+
image_bytes = await resp.read()
180+
181+
if path:
182+
Path(path).write_bytes(image_bytes)
183+
return image_bytes
184+
185+
async def download_comic(self, book_id: str) -> tuple[Path, str]:
186+
info = await self.comic_info(book_id)
187+
episodes = info["epsCount"]
188+
comic_name = f"{info['title']} - {info['author']}"
189+
tasks = []
190+
for char in path_filter:
191+
comic_name = comic_name.replace(char, " ")
192+
comic_path = CACHE_PATH / comic_name
193+
comic_path.mkdir(exist_ok=True)
194+
for episode in range(episodes):
195+
url = global_url / "comics" / book_id / "order" / str(episode + 1) / "pages"
196+
data = (await self.request(url))["data"]
197+
episode_title: str = data["ep"]["title"]
198+
episode_path = comic_path / episode_title
199+
episode_path.mkdir(exist_ok=True)
200+
for img in data["pages"]["docs"]:
201+
media = img["media"]
202+
img_url = f"{media['fileServer']}/static/{media['path']}"
203+
image_path: Path = episode_path / media["originalName"]
204+
if not image_path.exists():
205+
tasks.append([img_url, image_path])
206+
async with aiohttp.ClientSession(
207+
connector=TCPConnector(ssl=False, limit=5)
208+
) as session:
209+
tasks = [self.download_image_session(session, *t) for t in tasks]
210+
await asyncio.gather(*tasks)
211+
return comic_path, comic_name
212+
213+
214+
pica = Pica(username, password)
215+
# print(loop.run_until_complete(pica.search("SAGIRI")))
216+
# print(loop.run_until_complete(pica.categories()))
217+
# print(loop.run_until_complete(pica.random()))
218+
# print(loop.run_until_complete(pica.rank()))
219+
# print(loop.run_until_complete(pica.comic_info("5ce4d819431b5d017ddc8199")))
220+
# loop.run_until_complete(pica.download_comic("5821a1d55f6b9a4f93ef4a6b"))

0 commit comments

Comments
 (0)