| import hashlib |
| import hmac |
| from datetime import timedelta |
| from enum import Enum |
| from time import time |
| from typing import Any, Optional, cast |
|
|
| from httpx import URL |
|
|
| from hibiapi.api.bika.constants import BikaConstants |
| from hibiapi.api.bika.net import NetRequest |
| from hibiapi.utils.cache import cache_config |
| from hibiapi.utils.decorators import enum_auto_doc |
| from hibiapi.utils.net import catch_network_error |
| from hibiapi.utils.routing import BaseEndpoint, dont_route, request_headers |
|
|
|
|
| @enum_auto_doc |
| class ImageQuality(str, Enum): |
| """哔咔API返回的图片质量""" |
|
|
| low = "low" |
| """低质量""" |
| medium = "medium" |
| """中等质量""" |
| high = "high" |
| """高质量""" |
| original = "original" |
| """原图""" |
|
|
|
|
| @enum_auto_doc |
| class ResultSort(str, Enum): |
| """哔咔API返回的搜索结果排序方式""" |
|
|
| date_descending = "dd" |
| """最新发布""" |
| date_ascending = "da" |
| """最早发布""" |
| like_descending = "ld" |
| """最多喜欢""" |
| views_descending = "vd" |
| """最多浏览""" |
|
|
|
|
| class BikaEndpoints(BaseEndpoint): |
| @staticmethod |
| def _sign(url: URL, timestamp_bytes: bytes, nonce: bytes, method: bytes): |
| return hmac.new( |
| BikaConstants.DIGEST_KEY, |
| ( |
| url.raw_path.lstrip(b"/") |
| + timestamp_bytes |
| + nonce |
| + method |
| + BikaConstants.API_KEY |
| ).lower(), |
| hashlib.sha256, |
| ).hexdigest() |
|
|
| @dont_route |
| @catch_network_error |
| async def request( |
| self, |
| endpoint: str, |
| *, |
| params: Optional[dict[str, Any]] = None, |
| body: Optional[dict[str, Any]] = None, |
| no_token: bool = False, |
| ): |
| net_client = cast(NetRequest, self.client.net_client) |
| if not no_token: |
| async with net_client.auth_lock: |
| if net_client.token is None: |
| await net_client.login(self) |
|
|
| headers = { |
| "Authorization": net_client.token or "", |
| "Time": (current_time := f"{time():.0f}".encode()), |
| "Image-Quality": request_headers.get().get( |
| "X-Image-Quality", ImageQuality.medium |
| ), |
| "Nonce": (nonce := hashlib.md5(current_time).hexdigest().encode()), |
| "Signature": self._sign( |
| request_url := self._join( |
| base=BikaConstants.API_HOST, |
| endpoint=endpoint, |
| params=params or {}, |
| ), |
| current_time, |
| nonce, |
| b"GET" if body is None else b"POST", |
| ), |
| } |
|
|
| response = await ( |
| self.client.get(request_url, headers=headers) |
| if body is None |
| else self.client.post(request_url, headers=headers, json=body) |
| ) |
| return response.json() |
|
|
| @cache_config(ttl=timedelta(days=1)) |
| async def collections(self): |
| return await self.request("collections") |
|
|
| @cache_config(ttl=timedelta(days=3)) |
| async def categories(self): |
| return await self.request("categories") |
|
|
| @cache_config(ttl=timedelta(days=3)) |
| async def keywords(self): |
| return await self.request("keywords") |
|
|
| async def advanced_search( |
| self, |
| *, |
| keyword: str, |
| page: int = 1, |
| sort: ResultSort = ResultSort.date_descending, |
| ): |
| return await self.request( |
| "comics/advanced-search", |
| body={ |
| "keyword": keyword, |
| "sort": sort, |
| }, |
| params={ |
| "page": page, |
| "s": sort, |
| }, |
| ) |
|
|
| async def category_list( |
| self, |
| *, |
| category: str, |
| page: int = 1, |
| sort: ResultSort = ResultSort.date_descending, |
| ): |
| return await self.request( |
| "comics", |
| params={ |
| "page": page, |
| "c": category, |
| "s": sort, |
| }, |
| ) |
|
|
| async def author_list( |
| self, |
| *, |
| author: str, |
| page: int = 1, |
| sort: ResultSort = ResultSort.date_descending, |
| ): |
| return await self.request( |
| "comics", |
| params={ |
| "page": page, |
| "a": author, |
| "s": sort, |
| }, |
| ) |
|
|
| @cache_config(ttl=timedelta(days=3)) |
| async def comic_detail(self, *, id: str): |
| return await self.request("comics/{id}", params={"id": id}) |
|
|
| async def comic_recommendation(self, *, id: str): |
| return await self.request("comics/{id}/recommendation", params={"id": id}) |
|
|
| async def comic_episodes(self, *, id: str, page: int = 1): |
| return await self.request( |
| "comics/{id}/eps", |
| params={ |
| "id": id, |
| "page": page, |
| }, |
| ) |
|
|
| async def comic_page(self, *, id: str, order: int = 1, page: int = 1): |
| return await self.request( |
| "comics/{id}/order/{order}/pages", |
| params={ |
| "id": id, |
| "order": order, |
| "page": page, |
| }, |
| ) |
|
|
| async def comic_comments(self, *, id: str, page: int = 1): |
| return await self.request( |
| "comics/{id}/comments", |
| params={ |
| "id": id, |
| "page": page, |
| }, |
| ) |
|
|
| async def games(self, *, page: int = 1): |
| return await self.request("games", params={"page": page}) |
|
|
| @cache_config(ttl=timedelta(days=3)) |
| async def game_detail(self, *, id: str): |
| return await self.request("games/{id}", params={"id": id}) |
|
|