mirror of
https://github.com/PaiGramTeam/FixMiYouShe.git
synced 2024-11-21 23:18:03 +00:00
✨ support hoyolab
This commit is contained in:
parent
4be1f7c455
commit
e75c7f7943
@ -1,3 +1,5 @@
|
||||
DEBUG=False
|
||||
DOMAIN=127.0.0.1
|
||||
PORT=8080
|
||||
MIYOUSHE=true
|
||||
HOYOLAB=true
|
||||
|
12
main.py
12
main.py
@ -2,12 +2,18 @@ import asyncio
|
||||
import uvicorn
|
||||
|
||||
from src.app import app
|
||||
from src.env import PORT
|
||||
from src.render.article import refresh_recommend_posts
|
||||
from src.env import PORT, MIYOUSHE, HOYOLAB
|
||||
|
||||
|
||||
async def main():
|
||||
await refresh_recommend_posts()
|
||||
if MIYOUSHE:
|
||||
from src.render.article import refresh_recommend_posts
|
||||
|
||||
await refresh_recommend_posts()
|
||||
if HOYOLAB:
|
||||
from src.render.article_hoyolab import refresh_hoyo_recommend_posts
|
||||
|
||||
await refresh_hoyo_recommend_posts()
|
||||
web_server = uvicorn.Server(config=uvicorn.Config(app, host="0.0.0.0", port=PORT))
|
||||
server_config = web_server.config
|
||||
server_config.setup_event_loop()
|
||||
|
83
src/api/hoyolab.py
Normal file
83
src/api/hoyolab.py
Normal file
@ -0,0 +1,83 @@
|
||||
from typing import List
|
||||
|
||||
from .hyperionrequest import HyperionRequest
|
||||
from .models import PostInfo, PostRecommend, HoYoPostMultiLang
|
||||
|
||||
__all__ = ("Hoyolab",)
|
||||
|
||||
|
||||
class Hoyolab:
|
||||
POST_FULL_URL = "https://bbs-api-os.hoyolab.com/community/post/wapi/getPostFull"
|
||||
NEW_LIST_URL = "https://bbs-api-os.hoyolab.com/community/post/wapi/getNewsList"
|
||||
LANG = "zh-cn"
|
||||
USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/90.0.4430.72 Safari/537.36"
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.client = HyperionRequest(headers=self.get_headers(), *args, **kwargs)
|
||||
|
||||
def get_headers(self, lang: str = LANG):
|
||||
return {
|
||||
"User-Agent": self.USER_AGENT,
|
||||
"Referer": "https://www.hoyolab.com/",
|
||||
"X-Rpc-Language": lang,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_images_params(
|
||||
resize: int = 600,
|
||||
quality: int = 80,
|
||||
auto_orient: int = 0,
|
||||
interlace: int = 1,
|
||||
images_format: str = "jpg",
|
||||
) -> str:
|
||||
"""
|
||||
image/resize,s_600/quality,q_80/auto-orient,0/interlace,1/format,jpg
|
||||
:param resize: 图片大小
|
||||
:param quality: 图片质量
|
||||
:param auto_orient: 自适应
|
||||
:param interlace: 未知
|
||||
:param images_format: 图片格式
|
||||
:return:
|
||||
"""
|
||||
params = (
|
||||
f"image/resize,s_{resize}/quality,q_{quality}/auto-orient,"
|
||||
f"{auto_orient}/interlace,{interlace}/format,{images_format}"
|
||||
)
|
||||
return f"?x-oss-process={params}"
|
||||
|
||||
async def get_news_recommend(
|
||||
self, gids: int, page_size: int = 3, type_: int = 1
|
||||
) -> List[PostRecommend]:
|
||||
params = {"gids": gids, "page_size": page_size, "type": type_}
|
||||
response = await self.client.get(url=self.NEW_LIST_URL, params=params)
|
||||
return [
|
||||
PostRecommend(
|
||||
post_id=data["post"]["post_id"],
|
||||
subject=data["post"]["subject"],
|
||||
multi_language_info=HoYoPostMultiLang(
|
||||
**data["post"]["multi_language_info"]
|
||||
),
|
||||
)
|
||||
for data in response["list"]
|
||||
]
|
||||
|
||||
async def get_post_info(
|
||||
self, post_id: int, read: int = 1, scene: int = 1, lang: str = LANG
|
||||
) -> PostInfo:
|
||||
params = {"post_id": post_id, "read": read, "scene": scene}
|
||||
response = await self.client.get(
|
||||
self.POST_FULL_URL, params=params, headers=self.get_headers(lang=lang)
|
||||
)
|
||||
return PostInfo.paste_data(response, hoyolab=True)
|
||||
|
||||
async def close(self):
|
||||
await self.client.shutdown()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
@ -2,7 +2,6 @@ from typing import List
|
||||
|
||||
from .hyperionrequest import HyperionRequest
|
||||
from .models import PostInfo, PostRecommend
|
||||
from ..typedefs import JSON_DATA
|
||||
|
||||
__all__ = ("Hyperion",)
|
||||
|
||||
@ -14,10 +13,6 @@ class Hyperion:
|
||||
"""
|
||||
|
||||
POST_FULL_URL = "https://bbs-api.miyoushe.com/post/wapi/getPostFull"
|
||||
POST_FULL_IN_COLLECTION_URL = (
|
||||
"https://bbs-api.miyoushe.com/post/wapi/getPostFullInCollection"
|
||||
)
|
||||
GET_NEW_LIST_URL = "https://bbs-api.miyoushe.com/post/wapi/getNewsList"
|
||||
GET_OFFICIAL_RECOMMENDED_POSTS_URL = (
|
||||
"https://bbs-api.miyoushe.com/post/wapi/getOfficialRecommendedPosts"
|
||||
)
|
||||
@ -33,19 +28,6 @@ class Hyperion:
|
||||
def get_headers(self, referer: str = "https://www.miyoushe.com/ys/"):
|
||||
return {"User-Agent": self.USER_AGENT, "Referer": referer}
|
||||
|
||||
@staticmethod
|
||||
def get_list_url_params(
|
||||
forum_id: int, is_good: bool = False, is_hot: bool = False, page_size: int = 20
|
||||
) -> dict:
|
||||
return {
|
||||
"forum_id": forum_id,
|
||||
"gids": 2,
|
||||
"is_good": is_good,
|
||||
"is_hot": is_hot,
|
||||
"page_size": page_size,
|
||||
"sort_type": 1,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_images_params(
|
||||
resize: int = 600,
|
||||
@ -76,32 +58,16 @@ class Hyperion:
|
||||
)
|
||||
return [PostRecommend(**data) for data in response["list"]]
|
||||
|
||||
async def get_post_full_in_collection(
|
||||
self, collection_id: int, gids: int = 2, order_type=1
|
||||
) -> JSON_DATA:
|
||||
params = {
|
||||
"collection_id": collection_id,
|
||||
"gids": gids,
|
||||
"order_type": order_type,
|
||||
}
|
||||
response = await self.client.get(
|
||||
url=self.POST_FULL_IN_COLLECTION_URL, params=params
|
||||
)
|
||||
return response
|
||||
|
||||
async def get_post_info(self, gids: int, post_id: int, read: int = 1) -> PostInfo:
|
||||
params = {"gids": gids, "post_id": post_id, "read": read}
|
||||
response = await self.client.get(self.POST_FULL_URL, params=params)
|
||||
return PostInfo.paste_data(response)
|
||||
|
||||
async def get_new_list(self, gids: int, type_id: int, page_size: int = 20):
|
||||
"""
|
||||
?gids=2&page_size=20&type=3
|
||||
:return:
|
||||
"""
|
||||
params = {"gids": gids, "page_size": page_size, "type": type_id}
|
||||
response = await self.client.get(url=self.GET_NEW_LIST_URL, params=params)
|
||||
return response
|
||||
|
||||
async def close(self):
|
||||
await self.client.shutdown()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
|
94
src/api/i18n.py
Normal file
94
src/api/i18n.py
Normal file
@ -0,0 +1,94 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class I18nLang(str, Enum):
|
||||
ZH_CN = "zh-cn"
|
||||
ZH_TW = "zh-tw"
|
||||
DE_DE = "de-de"
|
||||
EN_US = "en-us"
|
||||
ES_ES = "es-es"
|
||||
FR_FR = "fr-fr"
|
||||
ID_ID = "id-id"
|
||||
IT_IT = "it-it"
|
||||
JA_JP = "ja-jp"
|
||||
KO_KR = "ko-kr"
|
||||
PT_PT = "pt-pt"
|
||||
RU_RU = "ru-ru"
|
||||
TH_TH = "th-th"
|
||||
TR_TR = "tr-tr"
|
||||
VI_VN = "vi-vn"
|
||||
|
||||
|
||||
i18n_map = {
|
||||
I18nLang.ZH_CN: {
|
||||
"view": "查看原文",
|
||||
"author": "作者信息",
|
||||
},
|
||||
I18nLang.ZH_TW: {
|
||||
"view": "查看原文",
|
||||
"author": "作者信息",
|
||||
},
|
||||
I18nLang.DE_DE: {
|
||||
"view": "Originaltext anzeigen",
|
||||
"author": "Informationen zum Autor",
|
||||
},
|
||||
I18nLang.EN_US: {
|
||||
"view": "View Original",
|
||||
"author": "Author Information",
|
||||
},
|
||||
I18nLang.ES_ES: {
|
||||
"view": "Ver original",
|
||||
"author": "Información del autor",
|
||||
},
|
||||
I18nLang.FR_FR: {
|
||||
"view": "Voir l'original",
|
||||
"author": "Informations sur l'auteur",
|
||||
},
|
||||
I18nLang.ID_ID: {
|
||||
"view": "Lihat aslinya",
|
||||
"author": "Informasi penulis",
|
||||
},
|
||||
I18nLang.IT_IT: {
|
||||
"view": "Visualizza originale",
|
||||
"author": "Informazioni sull'autore",
|
||||
},
|
||||
I18nLang.JA_JP: {
|
||||
"view": "元の記事を見る",
|
||||
"author": "作者情報",
|
||||
},
|
||||
I18nLang.KO_KR: {
|
||||
"view": "원본 보기",
|
||||
"author": "작성자 정보",
|
||||
},
|
||||
I18nLang.PT_PT: {
|
||||
"view": "Ver original",
|
||||
"author": "Informações do autor",
|
||||
},
|
||||
I18nLang.RU_RU: {
|
||||
"view": "Посмотреть оригинал",
|
||||
"author": "Информация об авторе",
|
||||
},
|
||||
I18nLang.TH_TH: {
|
||||
"view": "ดูต้นฉบับ",
|
||||
"author": "ข้อมูลผู้เขียน",
|
||||
},
|
||||
I18nLang.TR_TR: {
|
||||
"view": "Orijinali Görüntüle",
|
||||
"author": "Yazar Bilgisi",
|
||||
},
|
||||
I18nLang.VI_VN: {
|
||||
"view": "Xem bản gốc",
|
||||
"author": "Thông tin tác giả",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class I18n:
|
||||
def __init__(self, lang: str = "zh-cn"):
|
||||
self.lang = I18nLang(lang)
|
||||
|
||||
def get_property(self, name: str):
|
||||
return i18n_map.get(self.lang, {}).get(name, "")
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.get_property(item)
|
@ -1,11 +1,19 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr
|
||||
from pydantic import BaseModel, PrivateAttr, Field, AliasChoices
|
||||
|
||||
GAME_ID_MAP = {"bh3": 1, "ys": 2, "bh2": 3, "wd": 4, "dby": 5, "sr": 6, "zzz": 8}
|
||||
GAME_STR_MAP = {1: "bh3", 2: "ys", 3: "bh2", 4: "wd", 5: "dby", 6: "sr", 8: "zzz"}
|
||||
CHANNEL_MAP = {"ys": "yuanshen", "sr": "HSRCN", "zzz": "ZZZNewsletter"}
|
||||
__all__ = (
|
||||
"GAME_ID_MAP",
|
||||
"GAME_STR_MAP",
|
||||
"CHANNEL_MAP",
|
||||
"PostStat",
|
||||
"PostType",
|
||||
"HoYoPostMultiLang",
|
||||
"PostInfo",
|
||||
"PostRecommend",
|
||||
)
|
||||
@ -13,7 +21,9 @@ __all__ = (
|
||||
|
||||
class PostStat(BaseModel):
|
||||
reply_num: int = 0
|
||||
forward_num: int = 0
|
||||
forward_num: int = Field(
|
||||
default=0, validation_alias=AliasChoices("forward_num", "share_num")
|
||||
)
|
||||
like_num: int = 0
|
||||
view_num: int = 0
|
||||
bookmark_num: int = 0
|
||||
@ -22,6 +32,14 @@ class PostStat(BaseModel):
|
||||
class PostTopic(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
game_id_: int
|
||||
hoyolab: bool
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
if not self.hoyolab:
|
||||
return f"https://www.miyoushe.com/{self.game_id_}/topicDetail/{self.id}"
|
||||
return f"https://www.hoyolab.com/topicDetail/{self.id}"
|
||||
|
||||
|
||||
class PostType(int, Enum):
|
||||
@ -32,26 +50,70 @@ class PostType(int, Enum):
|
||||
VIDEO = 5
|
||||
|
||||
|
||||
class HoYoPostVideo(BaseModel):
|
||||
id: str
|
||||
cover: Optional[str]
|
||||
url: str
|
||||
|
||||
@property
|
||||
def is_youtube(self) -> bool:
|
||||
return "www.youtube.com" in self.url
|
||||
|
||||
|
||||
class HoYoPostMultiLang(BaseModel):
|
||||
lang_subject: dict
|
||||
|
||||
|
||||
class PostInfo(BaseModel):
|
||||
_data: dict = PrivateAttr()
|
||||
hoyolab: bool
|
||||
post_id: int
|
||||
user_uid: int
|
||||
subject: str
|
||||
image_urls: List[str]
|
||||
created_at: int
|
||||
created_at: datetime
|
||||
video_urls: List[str]
|
||||
content: str
|
||||
cover: Optional[str]
|
||||
game_id: int
|
||||
topics: List[PostTopic]
|
||||
view_type: PostType
|
||||
stat: PostStat
|
||||
video: Optional[HoYoPostVideo] = None
|
||||
|
||||
def __init__(self, _data: dict, **data: Any):
|
||||
super().__init__(**data)
|
||||
self._data = _data
|
||||
|
||||
@property
|
||||
def game_id_str(self) -> str:
|
||||
return GAME_STR_MAP.get(self.game_id, "")
|
||||
|
||||
@property
|
||||
def url_start(self) -> str:
|
||||
if not self.hoyolab:
|
||||
return f"{self.game_id_str}/article"
|
||||
return "article"
|
||||
|
||||
@property
|
||||
def url_path(self) -> str:
|
||||
return f"{self.url_start}/{self.post_id}"
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
if not self.hoyolab:
|
||||
return f"https://www.miyoushe.com/{self.url_path}"
|
||||
return f"https://www.hoyolab.com/{self.url_path}"
|
||||
|
||||
@property
|
||||
def author_url(self) -> str:
|
||||
author = self._data["post"]["user"]
|
||||
if not self.hoyolab:
|
||||
return f"https://www.miyoushe.com/{self.game_id_str}/accountCenter/postList?id={author['uid']}"
|
||||
return f"https://www.hoyolab.com/accountCenter/postList?id={author['uid']}"
|
||||
|
||||
@classmethod
|
||||
def paste_data(cls, data: dict) -> "PostInfo":
|
||||
def paste_data(cls, data: dict, hoyolab: bool = False) -> "PostInfo":
|
||||
_data_post = data["post"]
|
||||
post = _data_post["post"]
|
||||
post_id = post["post_id"]
|
||||
@ -62,18 +124,33 @@ class PostInfo(BaseModel):
|
||||
for image in image_list
|
||||
if abs(image["width"] - image["height"]) < 1300
|
||||
]
|
||||
vod_list = _data_post["vod_list"]
|
||||
vod_list = _data_post.get("vod_list", [])
|
||||
video_urls = [vod["resolutions"][-1]["url"] for vod in vod_list]
|
||||
created_at = post["created_at"]
|
||||
user = _data_post["user"] # 用户数据
|
||||
user_uid = user["uid"] # 用户ID
|
||||
content = post["content"]
|
||||
cover = post["cover"]
|
||||
topics = [PostTopic(**topic) for topic in _data_post["topics"]]
|
||||
cover_list = _data_post.get("cover_list", [])
|
||||
if (not cover) and cover_list:
|
||||
cover = cover_list[0]["url"]
|
||||
if (not cover) and image_urls:
|
||||
cover = image_urls[0]
|
||||
game_id = post["game_id"]
|
||||
topics = [
|
||||
PostTopic(game_id_=game_id, hoyolab=hoyolab, **topic)
|
||||
for topic in _data_post["topics"]
|
||||
]
|
||||
view_type = PostType(post["view_type"])
|
||||
stat = PostStat(**_data_post["stat"])
|
||||
video = (
|
||||
None
|
||||
if _data_post.get("video") is None
|
||||
else HoYoPostVideo(**_data_post["video"])
|
||||
)
|
||||
return PostInfo(
|
||||
_data=data,
|
||||
hoyolab=hoyolab,
|
||||
post_id=post_id,
|
||||
user_uid=user_uid,
|
||||
subject=subject,
|
||||
@ -82,9 +159,11 @@ class PostInfo(BaseModel):
|
||||
created_at=created_at,
|
||||
content=content,
|
||||
cover=cover,
|
||||
game_id=game_id,
|
||||
topics=topics,
|
||||
view_type=view_type,
|
||||
stat=stat,
|
||||
video=video,
|
||||
)
|
||||
|
||||
def __getitem__(self, item):
|
||||
@ -94,5 +173,6 @@ class PostInfo(BaseModel):
|
||||
class PostRecommend(BaseModel):
|
||||
post_id: int
|
||||
subject: str
|
||||
banner: Optional[str]
|
||||
official_type: Optional[int]
|
||||
banner: Optional[str] = None
|
||||
official_type: Optional[int] = None
|
||||
multi_language_info: Optional[HoYoPostMultiLang] = None
|
||||
|
@ -7,3 +7,5 @@ load_dotenv()
|
||||
DEBUG = os.getenv("DEBUG", "True").lower() == "true"
|
||||
DOMAIN = os.getenv("DOMAIN", "127.0.0.1")
|
||||
PORT = int(os.getenv("PORT", 8080))
|
||||
MIYOUSHE = os.getenv("MIYOUSHE", "True").lower() == "true"
|
||||
HOYOLAB = os.getenv("HOYOLAB", "True").lower() == "true"
|
||||
|
@ -1,20 +1,25 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Union, List, Dict, Optional
|
||||
from typing import Union, List, Dict, Callable
|
||||
|
||||
from bs4 import BeautifulSoup, Tag, PageElement
|
||||
|
||||
from src import template_env
|
||||
from src.api.hyperion import Hyperion
|
||||
from src.api.models import PostStat, PostInfo, PostType, PostRecommend
|
||||
from src.env import DOMAIN
|
||||
from src.api.i18n import I18n
|
||||
from src.api.models import (
|
||||
PostStat,
|
||||
PostInfo,
|
||||
PostType,
|
||||
PostRecommend,
|
||||
CHANNEL_MAP,
|
||||
GAME_ID_MAP,
|
||||
)
|
||||
from src.env import DOMAIN, MIYOUSHE
|
||||
from src.error import ArticleNotFoundError
|
||||
from src.log import logger
|
||||
from src.services.scheduler import scheduler
|
||||
|
||||
GAME_ID_MAP = {"bh3": 1, "ys": 2, "bh2": 3, "wd": 4, "dby": 5, "sr": 6, "zzz": 8}
|
||||
RECOMMEND_POST_MAP: Dict[str, List[PostRecommend]] = {}
|
||||
CHANNEL_MAP = {"ys": "yuanshen", "sr": "HSRCN", "zzz": "ZZZNewsletter"}
|
||||
template = template_env.get_template("article.jinja2")
|
||||
|
||||
|
||||
@ -64,6 +69,11 @@ def parse_tag(tag: Union[Tag, PageElement], post_info: PostInfo) -> str:
|
||||
if text := parse_tag(tag_, post_info):
|
||||
post_text.append(text)
|
||||
return "<p>" + "\n".join(post_text) + "</p>"
|
||||
elif tag.name == "iframe":
|
||||
src = tag.get("src")
|
||||
if src and "https://www.youtube.com" in src:
|
||||
return str(tag)
|
||||
return ""
|
||||
elif tag.name == "div":
|
||||
post_text = []
|
||||
for tag_ in tag.children:
|
||||
@ -96,43 +106,48 @@ def parse_stat(stat: PostStat):
|
||||
)
|
||||
|
||||
|
||||
def get_recommend_post(game_id: str, post_id: Optional[int]) -> List[PostRecommend]:
|
||||
posts = RECOMMEND_POST_MAP.get(game_id, [])
|
||||
if post_id:
|
||||
return [post for post in posts if post.post_id != post_id]
|
||||
def get_recommend_post(post_info: PostInfo, _: I18n) -> List[PostRecommend]:
|
||||
posts = RECOMMEND_POST_MAP.get(post_info.game_id_str, [])
|
||||
if post_info.post_id:
|
||||
return [post for post in posts if post.post_id != post_info.post_id]
|
||||
return posts
|
||||
|
||||
|
||||
def get_public_data(game_id: str, post_id: int, post_info: PostInfo) -> Dict:
|
||||
cover = post_info.cover
|
||||
if (not post_info.cover) and post_info.image_urls:
|
||||
cover = post_info.image_urls[0]
|
||||
def get_public_data(
|
||||
post_info: PostInfo,
|
||||
related_posts: Callable[[PostInfo, I18n], List[PostRecommend]],
|
||||
i18n: I18n,
|
||||
) -> Dict:
|
||||
return {
|
||||
"url": f"https://www.miyoushe.com/{game_id}/article/{post_id}",
|
||||
"published_time": datetime.fromtimestamp(post_info.created_at).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
),
|
||||
"channel": CHANNEL_MAP.get(game_id, "HSRCN"),
|
||||
"published_time": post_info.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
"channel": CHANNEL_MAP.get(post_info.game_id, "HSRCN"),
|
||||
"stat": parse_stat(post_info.stat),
|
||||
"game_id": game_id,
|
||||
"cover": cover,
|
||||
"post": post_info,
|
||||
"author": post_info["post"]["user"],
|
||||
"related_posts": get_recommend_post(game_id, post_id),
|
||||
"related_posts": related_posts(post_info, i18n),
|
||||
"DOMAIN": DOMAIN,
|
||||
"i18n": i18n,
|
||||
}
|
||||
|
||||
|
||||
async def process_article_text(game_id: str, post_id: int, post_info: PostInfo) -> str:
|
||||
async def process_article_text(
|
||||
post_info: PostInfo,
|
||||
related_posts: Callable[[PostInfo, I18n], List[PostRecommend]],
|
||||
i18n: I18n,
|
||||
) -> str:
|
||||
post_soup = BeautifulSoup(post_info.content, features="lxml")
|
||||
return template.render(
|
||||
description=get_description(post_soup),
|
||||
article=parse_content(post_soup, post_info),
|
||||
**get_public_data(game_id, post_id, post_info),
|
||||
**get_public_data(post_info, related_posts, i18n),
|
||||
)
|
||||
|
||||
|
||||
async def process_article_image(game_id: str, post_id: int, post_info: PostInfo) -> str:
|
||||
async def process_article_image(
|
||||
post_info: PostInfo,
|
||||
related_posts: Callable[[PostInfo, I18n], List[PostRecommend]],
|
||||
i18n: I18n,
|
||||
) -> str:
|
||||
json_data = json.loads(post_info.content)
|
||||
description = json_data.get("describe", "")
|
||||
article = ""
|
||||
@ -144,38 +159,34 @@ async def process_article_image(game_id: str, post_id: int, post_info: PostInfo)
|
||||
return template.render(
|
||||
description=description,
|
||||
article=article,
|
||||
**get_public_data(game_id, post_id, post_info),
|
||||
**get_public_data(post_info, related_posts, i18n),
|
||||
)
|
||||
|
||||
|
||||
async def process_article(game_id: str, post_id: int) -> str:
|
||||
async def process_article(game_id: str, post_id: int, i18n: I18n = I18n()) -> str:
|
||||
gids = GAME_ID_MAP.get(game_id)
|
||||
if not gids:
|
||||
raise ArticleNotFoundError(game_id, post_id)
|
||||
hyperion = Hyperion()
|
||||
try:
|
||||
async with Hyperion() as hyperion:
|
||||
post_info = await hyperion.get_post_info(gids=gids, post_id=post_id)
|
||||
finally:
|
||||
await hyperion.close()
|
||||
if post_info.view_type in [PostType.TEXT, PostType.VIDEO]:
|
||||
content = await process_article_text(game_id, post_id, post_info)
|
||||
content = await process_article_text(post_info, get_recommend_post, i18n)
|
||||
elif post_info.view_type == PostType.IMAGE:
|
||||
content = await process_article_image(game_id, post_id, post_info)
|
||||
content = await process_article_image(post_info, get_recommend_post, i18n)
|
||||
return content # noqa
|
||||
|
||||
|
||||
@scheduler.scheduled_job("cron", minute="0", second="10")
|
||||
async def refresh_recommend_posts():
|
||||
logger.info("Start to refresh recommend posts")
|
||||
hyperion = Hyperion()
|
||||
try:
|
||||
for key, gids in GAME_ID_MAP.items():
|
||||
try:
|
||||
RECOMMEND_POST_MAP[key] = await hyperion.get_official_recommended_posts(
|
||||
gids
|
||||
)
|
||||
except Exception as _:
|
||||
logger.exception(f"Failed to get recommend posts gids={gids}")
|
||||
finally:
|
||||
await hyperion.close()
|
||||
logger.info("Finish to refresh recommend posts")
|
||||
if MIYOUSHE:
|
||||
|
||||
@scheduler.scheduled_job("cron", minute="0", second="10")
|
||||
async def refresh_recommend_posts():
|
||||
logger.info("Start to refresh recommend posts")
|
||||
async with Hyperion() as hyperion:
|
||||
for key, gids in GAME_ID_MAP.items():
|
||||
try:
|
||||
RECOMMEND_POST_MAP[
|
||||
key
|
||||
] = await hyperion.get_official_recommended_posts(gids)
|
||||
except Exception as _:
|
||||
logger.exception(f"Failed to get recommend posts gids={gids}")
|
||||
logger.info("Finish to refresh recommend posts")
|
||||
|
88
src/render/article_hoyolab.py
Normal file
88
src/render/article_hoyolab.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
from typing import Dict, List, Callable
|
||||
|
||||
from src.api.hoyolab import Hoyolab
|
||||
from src.api.i18n import I18n
|
||||
from src.api.models import PostRecommend, PostType, PostInfo
|
||||
from src.env import HOYOLAB
|
||||
from src.log import logger
|
||||
from src.render.article import (
|
||||
process_article_text,
|
||||
process_article_image,
|
||||
template,
|
||||
get_public_data,
|
||||
)
|
||||
from src.services.scheduler import scheduler
|
||||
|
||||
GAME_ID_MAP = {"bh3": 1, "ys": 2, "wd": 4, "dby": 5, "sr": 6, "zzz": 8}
|
||||
RECOMMEND_POST_MAP: Dict[int, List[PostRecommend]] = {}
|
||||
|
||||
|
||||
def get_recommend_post(post_info: PostInfo, i18n: I18n) -> List[PostRecommend]:
|
||||
posts = RECOMMEND_POST_MAP.get(post_info.game_id, [])
|
||||
return [
|
||||
PostRecommend(
|
||||
post_id=post.post_id,
|
||||
subject=post.multi_language_info.lang_subject.get(
|
||||
i18n.lang.value, post.subject
|
||||
)
|
||||
if post.multi_language_info
|
||||
else post.subject,
|
||||
)
|
||||
for post in posts
|
||||
if post.post_id != post_info.post_id
|
||||
]
|
||||
|
||||
|
||||
async def process_article_video(
|
||||
post_info: PostInfo,
|
||||
related_posts: Callable[[PostInfo, I18n], List[PostRecommend]],
|
||||
i18n: I18n,
|
||||
) -> str:
|
||||
json_data = json.loads(post_info.content)
|
||||
description = json_data.get("describe", "")
|
||||
article = ""
|
||||
if post_info.video and post_info.video.is_youtube:
|
||||
article += f'<iframe allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="true" border="0" frameborder="0" framespacing="0" scrolling="no" src="{post_info.video.url}"></iframe>\n'
|
||||
if description:
|
||||
article += f"<p>{description}</p>\n"
|
||||
return template.render(
|
||||
description=description,
|
||||
article=article,
|
||||
**get_public_data(post_info, related_posts, i18n),
|
||||
)
|
||||
|
||||
|
||||
async def process_article(post_id: int, lang: str) -> str:
|
||||
try:
|
||||
i18n = I18n(lang)
|
||||
except ValueError:
|
||||
i18n = I18n()
|
||||
async with Hoyolab() as hoyolab:
|
||||
post_info = await hoyolab.get_post_info(post_id=post_id, lang=i18n.lang.value)
|
||||
if post_info.view_type == PostType.TEXT:
|
||||
content = await process_article_text(post_info, get_recommend_post, i18n)
|
||||
elif post_info.view_type == PostType.IMAGE:
|
||||
content = await process_article_image(post_info, get_recommend_post, i18n)
|
||||
elif post_info.view_type == PostType.VIDEO:
|
||||
content = await process_article_video(post_info, get_recommend_post, i18n)
|
||||
return content # noqa
|
||||
|
||||
|
||||
if HOYOLAB:
|
||||
|
||||
@scheduler.scheduled_job("cron", minute="0", second="10")
|
||||
async def refresh_hoyo_recommend_posts():
|
||||
logger.info("Start to refresh hoyolab recommend posts")
|
||||
async with Hoyolab() as hoyolab:
|
||||
for gids in GAME_ID_MAP.values():
|
||||
temp = []
|
||||
for k in (1, 2, 3):
|
||||
try:
|
||||
temp.extend(await hoyolab.get_news_recommend(gids, type_=k))
|
||||
except Exception as _:
|
||||
logger.exception(
|
||||
f"Failed to get recommend posts gids={gids} type={k}"
|
||||
)
|
||||
RECOMMEND_POST_MAP[gids] = temp
|
||||
logger.info("Finish to refresh hoyolab recommend posts")
|
@ -1,8 +1,18 @@
|
||||
from src.env import MIYOUSHE, HOYOLAB
|
||||
|
||||
|
||||
def get_routes():
|
||||
from .article import parse_article
|
||||
from .error import validation_exception_handler
|
||||
|
||||
return [
|
||||
parse_article,
|
||||
routes = [
|
||||
validation_exception_handler,
|
||||
]
|
||||
|
||||
if MIYOUSHE:
|
||||
from .article import parse_article
|
||||
|
||||
routes.append(parse_article)
|
||||
if HOYOLAB:
|
||||
from .article_hoyolab import parse_hoyo_article
|
||||
|
||||
routes.append(parse_hoyo_article)
|
||||
|
24
src/route/article_hoyolab.py
Normal file
24
src/route/article_hoyolab.py
Normal file
@ -0,0 +1,24 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from .base import get_redirect_response
|
||||
from ..app import app
|
||||
from ..error import ArticleError, ResponseException
|
||||
from ..log import logger
|
||||
from ..render.article_hoyolab import process_article
|
||||
|
||||
|
||||
@app.get("/article/{post_id}")
|
||||
@app.get("/article/{post_id}/{lang}")
|
||||
async def parse_hoyo_article(post_id: int, request: Request, lang: str = "zh-cn"):
|
||||
try:
|
||||
return HTMLResponse(await process_article(post_id, lang))
|
||||
except ResponseException as e:
|
||||
logger.warning(e.message)
|
||||
return get_redirect_response(request)
|
||||
except ArticleError as e:
|
||||
logger.warning(e.msg)
|
||||
return get_redirect_response(request)
|
||||
except Exception as _:
|
||||
logger.exception(f"Failed to get article {post_id} lang {lang}")
|
||||
return get_redirect_response(request)
|
@ -3,15 +3,19 @@ from typing import TYPE_CHECKING
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import RedirectResponse
|
||||
|
||||
from src.env import MIYOUSHE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from starlette.middleware.base import RequestResponseEndpoint
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
BASE_URL = "https://www.miyoushe.com" if MIYOUSHE else "https://www.hoyolab.com"
|
||||
|
||||
|
||||
def get_redirect_response(request: "Request") -> RedirectResponse:
|
||||
path = request.url.path
|
||||
return RedirectResponse(url=f"https://www.miyoushe.com{path}", status_code=302)
|
||||
return RedirectResponse(url=f"{BASE_URL}{path}", status_code=302)
|
||||
|
||||
|
||||
class UserAgentMiddleware(BaseHTTPMiddleware):
|
||||
|
@ -13,16 +13,16 @@ Embed MiYouShe posts, videos, polls, and more on Telegram
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<link rel="canonical" href="{{ url }}"/>
|
||||
<link rel="canonical" href="{{ post.url }}"/>
|
||||
<meta property="theme-color" content="#00a8fc"/>
|
||||
<meta property="twitter:site" content="{{ author.nickname }}"/>
|
||||
<meta property="twitter:creator" content="{{ author.nickname }}"/>
|
||||
<meta property="twitter:title" content="{{ post.subject }} ({{ author.nickname }})"/>
|
||||
<meta property="twitter:image" content="{{ cover }}"/>
|
||||
<meta property="twitter:image" content="{{ post.cover }}"/>
|
||||
<meta property="twitter:card" content="summary_large_image"/>
|
||||
|
||||
<meta property="og:url" content="{{ url }}"/>
|
||||
<meta property="og:image" content="{{ cover }}"/>
|
||||
<meta property="og:url" content="{{ post.url }}"/>
|
||||
<meta property="og:image" content="{{ post.cover }}"/>
|
||||
<meta property="og:title" content="{{ post.subject }} ({{ author.nickname }})"/>
|
||||
<meta property="og:description" content="{{ description }}"/>
|
||||
<meta property="og:site_name" content="{{ post.subject }} - {{ author.nickname }} - 米游社"/>
|
||||
@ -39,7 +39,7 @@ Embed MiYouShe posts, videos, polls, and more on Telegram
|
||||
</section>
|
||||
<section class="section--first">
|
||||
If you can see this, your browser is doing something weird with your user agent.
|
||||
<a href="{{ url }}">View original post</a>
|
||||
<a href="{{ post.url }}">View original post</a>
|
||||
</section>
|
||||
<article>
|
||||
<!-- article content -->
|
||||
@ -50,28 +50,33 @@ Embed MiYouShe posts, videos, polls, and more on Telegram
|
||||
{% if post.topics %}
|
||||
<p>
|
||||
{% for topic in post.topics %}
|
||||
<a href="https://www.miyoushe.com/{{ game_id }}/topicDetail/{{ topic.id }}">#{{ topic.name }} </a>
|
||||
<a href="{{ topic.url }}">#{{ topic.name }} </a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p><a href="{{ url }}">查看原文</a></p>
|
||||
<p><a href="{{ post.url }}">{{ i18n.view }}</a></p>
|
||||
<!-- author -->
|
||||
<details>
|
||||
<summary>作者信息</summary>
|
||||
<summary>{{ i18n.author }}</summary>
|
||||
{% if author.avatar_url %}
|
||||
<img src="{{ author.avatar_url }}" alt="profile picture"/>
|
||||
{% endif %}
|
||||
<h2>{{ author.nickname }}</h2>
|
||||
<p>
|
||||
<a href="https://www.miyoushe.com/{{ game_id }}/accountCenter/postList?id={{ author.uid }}">@{{ author.nickname }}</a>
|
||||
<a href="{{ post.author_url }}">@{{ author.nickname }}</a>
|
||||
lv.{{ author.level_exp.level }}
|
||||
</p>
|
||||
</details>
|
||||
<!-- related posts -->
|
||||
{% if related_posts %}
|
||||
{% for post in related_posts %}
|
||||
{% for post_ in related_posts %}
|
||||
<related>
|
||||
<a href="https://{{ DOMAIN }}/{{ game_id }}/article/{{ post.post_id }}">{{ post.subject }}</a>
|
||||
{% if i18n.lang.value == 'zh-cn' %}
|
||||
{% set ends = '' %}
|
||||
{% else %}
|
||||
{% set ends = '/' + i18n.lang.value %}
|
||||
{% endif %}
|
||||
<a href="https://{{ DOMAIN }}/{{ post.url_start }}/{{ post_.post_id }}{{ ends }}">{{ post_.subject }}</a>
|
||||
</related>
|
||||
<br/>
|
||||
{% endfor %}
|
||||
|
0
src/utils/__init__.py
Normal file
0
src/utils/__init__.py
Normal file
0
src/utils/article.py
Normal file
0
src/utils/article.py
Normal file
Loading…
Reference in New Issue
Block a user