DengFengLai's picture
DF.
0a1b571
raw
history blame
17.6 kB
import json
import re
from datetime import date, timedelta
from enum import Enum
from typing import Any, Literal, Optional, Union, cast, overload
from hibiapi.api.pixiv.constants import PixivConstants
from hibiapi.api.pixiv.net import NetRequest as PixivNetClient
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 IllustType(str, Enum):
"""็”ปไฝœ็ฑปๅž‹"""
illust = "illust"
"""ๆ’็”ป"""
manga = "manga"
"""ๆผซ็”ป"""
@enum_auto_doc
class RankingType(str, Enum):
"""ๆŽ’่กŒๆฆœๅ†…ๅฎน็ฑปๅž‹"""
day = "day"
"""ๆ—ฅๆฆœ"""
week = "week"
"""ๅ‘จๆฆœ"""
month = "month"
"""ๆœˆๆฆœ"""
day_male = "day_male"
"""็”ทๆ€งๅ‘"""
day_female = "day_female"
"""ๅฅณๆ€งๅ‘"""
week_original = "week_original"
"""ๅŽŸๅˆ›ๅ‘จๆฆœ"""
week_rookie = "week_rookie"
"""ๆ–ฐไบบๅ‘จๆฆœ"""
day_ai = "day_ai"
"""AIๆ—ฅๆฆœ"""
day_manga = "day_manga"
"""ๆผซ็”ปๆ—ฅๆฆœ"""
week_manga = "week_manga"
"""ๆผซ็”ปๅ‘จๆฆœ"""
month_manga = "month_manga"
"""ๆผซ็”ปๆœˆๆฆœ"""
week_rookie_manga = "week_rookie_manga"
"""ๆผซ็”ปๆ–ฐไบบๅ‘จๆฆœ"""
day_r18 = "day_r18"
day_male_r18 = "day_male_r18"
day_female_r18 = "day_female_r18"
week_r18 = "week_r18"
week_r18g = "week_r18g"
day_r18_ai = "day_r18_ai"
day_r18_manga = "day_r18_manga"
week_r18_manga = "week_r18_manga"
@enum_auto_doc
class SearchModeType(str, Enum):
"""ๆœ็ดขๅŒน้…็ฑปๅž‹"""
partial_match_for_tags = "partial_match_for_tags"
"""ๆ ‡็ญพ้ƒจๅˆ†ไธ€่‡ด"""
exact_match_for_tags = "exact_match_for_tags"
"""ๆ ‡็ญพๅฎŒๅ…จไธ€่‡ด"""
title_and_caption = "title_and_caption"
"""ๆ ‡้ข˜่ฏดๆ˜Žๆ–‡"""
@enum_auto_doc
class SearchNovelModeType(str, Enum):
"""ๆœ็ดขๅŒน้…็ฑปๅž‹"""
partial_match_for_tags = "partial_match_for_tags"
"""ๆ ‡็ญพ้ƒจๅˆ†ไธ€่‡ด"""
exact_match_for_tags = "exact_match_for_tags"
"""ๆ ‡็ญพๅฎŒๅ…จไธ€่‡ด"""
text = "text"
"""ๆญฃๆ–‡"""
keyword = "keyword"
"""ๅ…ณ้”ฎ่ฏ"""
@enum_auto_doc
class SearchSortType(str, Enum):
"""ๆœ็ดขๆŽ’ๅบ็ฑปๅž‹"""
date_desc = "date_desc"
"""ๆŒ‰ๆ—ฅๆœŸๅ€’ๅบ"""
date_asc = "date_asc"
"""ๆŒ‰ๆ—ฅๆœŸๆญฃๅบ"""
popular_desc = "popular_desc"
"""ๅ—ๆฌข่ฟŽ้™ๅบ(PremiumๅŠŸ่ƒฝ)"""
@enum_auto_doc
class SearchDurationType(str, Enum):
"""ๆœ็ดขๆ—ถๆฎต็ฑปๅž‹"""
within_last_day = "within_last_day"
"""ไธ€ๅคฉๅ†…"""
within_last_week = "within_last_week"
"""ไธ€ๅ‘จๅ†…"""
within_last_month = "within_last_month"
"""ไธ€ไธชๆœˆๅ†…"""
class RankingDate(date):
@classmethod
def yesterday(cls) -> "RankingDate":
yesterday = cls.today() - timedelta(days=1)
return cls(yesterday.year, yesterday.month, yesterday.day)
def toString(self) -> str:
return self.strftime(r"%Y-%m-%d")
@classmethod
def new(cls, date: date) -> "RankingDate":
return cls(date.year, date.month, date.day)
class PixivEndpoints(BaseEndpoint):
@staticmethod
def _parse_accept_language(accept_language: str) -> str:
first_language, *_ = accept_language.partition(",")
language_code, *_ = first_language.partition(";")
return language_code.lower().strip()
@overload
async def request(
self,
endpoint: str,
*,
params: Optional[dict[str, Any]] = None,
return_text: Literal[False] = False,
) -> dict[str, Any]: ...
@overload
async def request(
self,
endpoint: str,
*,
params: Optional[dict[str, Any]] = None,
return_text: Literal[True],
) -> str: ...
@dont_route
@catch_network_error
async def request(
self,
endpoint: str,
*,
params: Optional[dict[str, Any]] = None,
return_text: bool = False,
) -> Union[dict[str, Any], str]:
headers = self.client.headers.copy()
net_client = cast(PixivNetClient, self.client.net_client)
async with net_client.auth_lock:
auth, token = net_client.get_available_user()
if auth is None:
auth = await net_client.auth(token)
headers["Authorization"] = f"Bearer {auth.access_token}"
if language := request_headers.get().get("Accept-Language"):
language = self._parse_accept_language(language)
headers["Accept-Language"] = language
response = await self.client.get(
self._join(
base=PixivConstants.APP_HOST,
endpoint=endpoint,
params=params or {},
),
headers=headers,
)
if return_text:
return response.text
return response.json()
@cache_config(ttl=timedelta(days=3))
async def illust(self, *, id: int):
return await self.request("v1/illust/detail", params={"illust_id": id})
@cache_config(ttl=timedelta(days=1))
async def member(self, *, id: int):
return await self.request("v1/user/detail", params={"user_id": id})
async def member_illust(
self,
*,
id: int,
illust_type: IllustType = IllustType.illust,
page: int = 1,
size: int = 30,
):
return await self.request(
"v1/user/illusts",
params={
"user_id": id,
"type": illust_type,
"offset": (page - 1) * size,
},
)
async def favorite(
self,
*,
id: int,
tag: Optional[str] = None,
max_bookmark_id: Optional[int] = None,
):
return await self.request(
"v1/user/bookmarks/illust",
params={
"user_id": id,
"tag": tag,
"restrict": "public",
"max_bookmark_id": max_bookmark_id or None,
},
)
# ็”จๆˆทๆ”ถ่—็š„ๅฐ่ฏด
async def favorite_novel(
self,
*,
id: int,
tag: Optional[str] = None,
):
return await self.request(
"v1/user/bookmarks/novel",
params={
"user_id": id,
"tag": tag,
"restrict": "public",
},
)
async def following(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v1/user/following",
params={
"user_id": id,
"offset": (page - 1) * size,
},
)
async def follower(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v1/user/follower",
params={
"user_id": id,
"offset": (page - 1) * size,
},
)
@cache_config(ttl=timedelta(hours=12))
async def rank(
self,
*,
mode: RankingType = RankingType.week,
date: Optional[RankingDate] = None,
page: int = 1,
size: int = 30,
):
return await self.request(
"v1/illust/ranking",
params={
"mode": mode,
"date": RankingDate.new(date or RankingDate.yesterday()).toString(),
"offset": (page - 1) * size,
},
)
async def search(
self,
*,
word: str,
mode: SearchModeType = SearchModeType.partial_match_for_tags,
order: SearchSortType = SearchSortType.date_desc,
duration: Optional[SearchDurationType] = None,
page: int = 1,
size: int = 30,
include_translated_tag_results: bool = True,
search_ai_type: bool = True, # ๆœ็ดข็ป“ๆžœๆ˜ฏๅฆๅŒ…ๅซAIไฝœๅ“
):
return await self.request(
"v1/search/illust",
params={
"word": word,
"search_target": mode,
"sort": order,
"duration": duration,
"offset": (page - 1) * size,
"include_translated_tag_results": include_translated_tag_results,
"search_ai_type": 1 if search_ai_type else 0,
},
)
# ็ƒญ้—จๆ’็”ปไฝœๅ“้ข„่งˆ
async def popular_preview(
self,
*,
word: str,
mode: SearchModeType = SearchModeType.partial_match_for_tags,
merge_plain_keyword_results: bool = True,
include_translated_tag_results: bool = True,
filter: str = "for_ios",
):
return await self.request(
"v1/search/popular-preview/illust",
params={
"word": word,
"search_target": mode,
"merge_plain_keyword_results": merge_plain_keyword_results,
"include_translated_tag_results": include_translated_tag_results,
"filter": filter,
},
)
async def search_user(
self,
*,
word: str,
page: int = 1,
size: int = 30,
):
return await self.request(
"v1/search/user",
params={"word": word, "offset": (page - 1) * size},
)
async def tags_autocomplete(
self,
*,
word: str,
merge_plain_keyword_results: bool = True,
):
return await self.request(
"/v2/search/autocomplete",
params={
"word": word,
"merge_plain_keyword_results": merge_plain_keyword_results,
},
)
@cache_config(ttl=timedelta(hours=12))
async def tags(self):
return await self.request("v1/trending-tags/illust")
@cache_config(ttl=timedelta(minutes=15))
async def related(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v2/illust/related",
params={
"illust_id": id,
"offset": (page - 1) * size,
},
)
@cache_config(ttl=timedelta(days=3))
async def ugoira_metadata(self, *, id: int):
return await self.request(
"v1/ugoira/metadata",
params={
"illust_id": id,
},
)
# ๅคงๅฎถ็š„ๆ–ฐไฝœๅ“๏ผˆๆ’็”ป๏ผ‰
async def illust_new(
self,
*,
content_type: str = "illust",
):
return await self.request(
"v1/illust/new",
params={
"content_type": content_type,
"filter": "for_ios",
},
)
# pixivision(ไบฎ็‚น/็‰น่พ‘) ๅˆ—่กจ
async def spotlights(
self,
*,
category: str = "all",
page: int = 1,
size: int = 10,
):
return await self.request(
"v1/spotlight/articles",
params={
"filter": "for_ios",
"category": category,
"offset": (page - 1) * size,
},
)
# ๆ’็”ป่ฏ„่ฎบ
async def illust_comments(
self,
*,
id: int,
page: int = 1,
size: int = 30,
):
return await self.request(
"v3/illust/comments",
params={
"illust_id": id,
"offset": (page - 1) * size,
},
)
# ๆ’็”ป่ฏ„่ฎบๅ›žๅค
async def illust_comment_replies(
self,
*,
id: int,
):
return await self.request(
"v2/illust/comment/replies",
params={
"comment_id": id,
},
)
# ๅฐ่ฏด่ฏ„่ฎบ
async def novel_comments(
self,
*,
id: int,
page: int = 1,
size: int = 30,
):
return await self.request(
"v3/novel/comments",
params={
"novel_id": id,
"offset": (page - 1) * size,
},
)
# ๅฐ่ฏด่ฏ„่ฎบๅ›žๅค
async def novel_comment_replies(
self,
*,
id: int,
):
return await self.request(
"v2/novel/comment/replies",
params={
"comment_id": id,
},
)
# ๅฐ่ฏดๆŽ’่กŒๆฆœ
async def rank_novel(
self,
*,
mode: str = "day",
date: Optional[RankingDate] = None,
page: int = 1,
size: int = 30,
):
return await self.request(
"v1/novel/ranking",
params={
"mode": mode,
"date": RankingDate.new(date or RankingDate.yesterday()).toString(),
"offset": (page - 1) * size,
},
)
async def member_novel(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"/v1/user/novels",
params={
"user_id": id,
"offset": (page - 1) * size,
},
)
async def novel_series(self, *, id: int):
return await self.request("/v2/novel/series", params={"series_id": id})
async def novel_detail(self, *, id: int):
return await self.request("/v2/novel/detail", params={"novel_id": id})
# ๅทฒ่ขซๅฎ˜ๆ–น็งป้™ค๏ผŒ่ฐƒ็”จ webview/v2/novel ไฝœๅ…ผๅฎนๅค„็†
async def novel_text(self, *, id: int):
# return await self.request("/v1/novel/text", params={"novel_id": id})
response = await self.webview_novel(id=id)
return {"novel_text": response["text"] or ""}
# ่Žทๅ–ๅฐ่ฏด HTML ๅŽ่งฃๆž JSON
async def webview_novel(self, *, id: int):
response = await self.request(
"webview/v2/novel",
params={
"id": id,
"viewer_version": "20221031_ai",
},
return_text=True,
)
novel_match = re.search(r"novel:\s+(?P<data>{.+?}),\s+isOwnWork", response)
return json.loads(novel_match["data"] if novel_match else response)
@cache_config(ttl=timedelta(hours=12))
async def tags_novel(self):
return await self.request("v1/trending-tags/novel")
async def search_novel(
self,
*,
word: str,
mode: SearchNovelModeType = SearchNovelModeType.partial_match_for_tags,
sort: SearchSortType = SearchSortType.date_desc,
merge_plain_keyword_results: bool = True,
include_translated_tag_results: bool = True,
duration: Optional[SearchDurationType] = None,
page: int = 1,
size: int = 30,
search_ai_type: bool = True, # ๆœ็ดข็ป“ๆžœๆ˜ฏๅฆๅŒ…ๅซAIไฝœๅ“
):
return await self.request(
"/v1/search/novel",
params={
"word": word,
"search_target": mode,
"sort": sort,
"merge_plain_keyword_results": merge_plain_keyword_results,
"include_translated_tag_results": include_translated_tag_results,
"duration": duration,
"offset": (page - 1) * size,
"search_ai_type": 1 if search_ai_type else 0,
},
)
# ็ƒญ้—จๅฐ่ฏดไฝœๅ“้ข„่งˆ
async def popular_preview_novel(
self,
*,
word: str,
mode: SearchNovelModeType = SearchNovelModeType.partial_match_for_tags,
merge_plain_keyword_results: bool = True,
include_translated_tag_results: bool = True,
filter: str = "for_ios",
):
return await self.request(
"v1/search/popular-preview/novel",
params={
"word": word,
"search_target": mode,
"merge_plain_keyword_results": merge_plain_keyword_results,
"include_translated_tag_results": include_translated_tag_results,
"filter": filter,
},
)
async def novel_new(self, *, max_novel_id: Optional[int] = None):
return await self.request(
"/v1/novel/new", params={"max_novel_id": max_novel_id}
)
# ไบบๆฐ”็›ดๆ’ญๅˆ—่กจ
async def live_list(self, *, page: int = 1, size: int = 30):
params = {"list_type": "popular", "offset": (page - 1) * size}
if not params["offset"]:
del params["offset"]
return await self.request("v1/live/list", params=params)
# ็›ธๅ…ณๅฐ่ฏดไฝœๅ“
async def related_novel(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v1/novel/related",
params={
"novel_id": id,
"offset": (page - 1) * size,
},
)
# ็›ธๅ…ณ็”จๆˆท
async def related_member(self, *, id: int):
return await self.request("v1/user/related", params={"seed_user_id": id})
# ๆผซ็”ป็ณปๅˆ—
async def illust_series(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v1/illust/series",
params={"illust_series_id": id, "offset": (page - 1) * size},
)
# ็”จๆˆท็š„ๆผซ็”ป็ณปๅˆ—
async def member_illust_series(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v1/user/illust-series",
params={"user_id": id, "offset": (page - 1) * size},
)
# ็”จๆˆท็š„ๅฐ่ฏด็ณปๅˆ—
async def member_novel_series(self, *, id: int, page: int = 1, size: int = 30):
return await self.request(
"v1/user/novel-series", params={"user_id": id, "offset": (page - 1) * size}
)