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})