Skip to content

Commit e124d53

Browse files
committed
feat: experimental htmlkit
1 parent 385e211 commit e124d53

File tree

6 files changed

+202
-21
lines changed

6 files changed

+202
-21
lines changed

pdm.lock

Lines changed: 27 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies = [
1010
"nonebot-adapter-milky~=0.5.0rc3",
1111
"nonebot-plugin-alconna~=0.59",
1212
"nonebot-plugin-apscheduler~=0.5",
13+
"nonebot-plugin-htmlkit<1,>=0.1.0rc1",
1314
"nonebot-plugin-htmlrender~=0.6",
1415
"nonebot-plugin-orm[sqlite]~=0.8",
1516
"nonebot-plugin-uninfo @ https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo/archive/5b87d916af09d6a878039d3445193efda2ed1af3.zip",
@@ -30,6 +31,7 @@ adapters = [
3031
plugins = [
3132
"nonebot_plugin_alconna",
3233
"nonebot_plugin_apscheduler",
34+
"nonebot_plugin_htmlkit",
3335
"nonebot_plugin_htmlrender",
3436
"nonebot_plugin_orm",
3537
"nonebot_plugin_uninfo",

src/plugins/bilibili/plugins/dynamic/__init__.py

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from asyncio import gather
22
from contextlib import asynccontextmanager
3+
from importlib.resources import as_file, files
34
from queue import PriorityQueue
45
from typing import Annotated, Any, AsyncGenerator
56

@@ -11,16 +12,18 @@
1112
from nonebot.plugin import PluginMetadata
1213
from nonebot_plugin_alconna import Alconna, Image, Subcommand, UniMessage, on_alconna
1314
from nonebot_plugin_apscheduler import scheduler
15+
from nonebot_plugin_htmlkit import template_to_pic
1416
from nonebot_plugin_htmlrender.browser import get_browser
1517
from nonebot_plugin_orm import async_scoped_session, get_session
1618
from nonebot_plugin_uninfo import MEMBER
1719
from nonebot_plugin_uninfo.orm import SceneModel, SceneOrm
1820
from playwright.async_api import BrowserContext, Page
1921
from sqlalchemy import exists, select
2022

21-
from .....utils import run_task, send_message
23+
from .....utils import css_fetch_fn, img_fetch_fn, run_task, send_message
2224
from ... import plugin_config as bilibili_config
2325
from ...utils import UID_ARG, get_share_click, handle_error, raise_for_status
26+
from . import templates
2427
from .config import Config
2528
from .models import Dynamic, Dynamics, Subscription
2629

@@ -163,7 +166,7 @@ async def render_screenshot(id_str: str) -> bytes:
163166
)
164167
await page.add_style_tag(
165168
content="""
166-
* {
169+
body {
167170
font-family: "LXGW ZhenKai GB", "LXGW WenKai GB", sans-serif !important;
168171
}
169172
@@ -198,15 +201,40 @@ async def render_screenshot(id_str: str) -> bytes:
198201
async def broadcast(dynamics: list[Dynamic]):
199202
async with get_session() as session:
200203
for dynamic in dynamics:
201-
screenshot, url, subs = await gather(
202-
render_screenshot(dynamic["id_str"]),
203-
get_share_click(dynamic["id_str"], "dynamic", "dt.dt-detail.0.0.pv"),
204-
session.scalars(
205-
select(Subscription).where(
206-
Subscription.uid == dynamic["modules"]["module_author"]["mid"]
207-
)
208-
),
209-
)
204+
with as_file(files(templates)) as templates_path:
205+
screenshot, url, subs = await gather(
206+
(
207+
template_to_pic(
208+
str(templates_path),
209+
"draw.html.j2",
210+
{"dynamic": dynamic},
211+
max_width=360 * 3,
212+
device_height=640 * 3,
213+
allow_refit=False,
214+
img_fetch_fn=img_fetch_fn,
215+
css_fetch_fn=css_fetch_fn,
216+
image_format="jpeg",
217+
jpeg_quality=80,
218+
)
219+
if dynamic["type"] == "DYNAMIC_TYPE_DRAW"
220+
and not (
221+
dynamic["modules"]["module_dynamic"]["additional"]
222+
or dynamic["modules"]["module_dynamic"]["major"]["opus"][
223+
"pics"
224+
]
225+
)
226+
else render_screenshot(dynamic["id_str"])
227+
),
228+
get_share_click(
229+
dynamic["id_str"], "dynamic", "dt.dt-detail.0.0.pv"
230+
),
231+
session.scalars(
232+
select(Subscription).where(
233+
Subscription.uid
234+
== dynamic["modules"]["module_author"]["mid"]
235+
)
236+
),
237+
)
210238

211239
msg = plugin_config.template.format(
212240
name=dynamic["modules"]["module_author"]["name"],
@@ -316,10 +344,30 @@ async def _(id_str: str):
316344
except Exception:
317345
await handle_error("获取动态信息失败")
318346

319-
screenshot, url = await gather(
320-
render_screenshot(id_str),
321-
get_share_click(id_str, "dynamic", "dt.dt-detail.0.0.pv"),
322-
)
347+
with as_file(files(templates)) as templates_path:
348+
screenshot, url = await gather(
349+
(
350+
template_to_pic(
351+
str(templates_path),
352+
"draw.html.j2",
353+
{"dynamic": dynamic},
354+
max_width=360 * 3,
355+
device_height=640 * 3,
356+
allow_refit=False,
357+
img_fetch_fn=img_fetch_fn,
358+
css_fetch_fn=css_fetch_fn,
359+
image_format="jpeg",
360+
jpeg_quality=80,
361+
)
362+
if dynamic["type"] == "DYNAMIC_TYPE_DRAW"
363+
and not (
364+
dynamic["modules"]["module_dynamic"]["additional"]
365+
or dynamic["modules"]["module_dynamic"]["major"]["opus"]["pics"]
366+
)
367+
else render_screenshot(dynamic["id_str"])
368+
),
369+
get_share_click(id_str, "dynamic", "dt.dt-detail.0.0.pv"),
370+
)
323371
await plugin_config.template.format(
324372
name=dynamic["modules"]["module_author"]["name"],
325373
action=dynamic["modules"]["module_author"]["pub_action"]

src/plugins/bilibili/plugins/dynamic/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TypedDict
1+
from typing import Any, TypedDict
22

33
from nonebot_plugin_orm import Model
44
from nonebot_plugin_uninfo.orm import SceneModel
@@ -14,6 +14,7 @@ class ModuleAuthor(TypedDict):
1414

1515
class Modules(TypedDict):
1616
module_author: ModuleAuthor
17+
module_dynamic: dict[str, Any]
1718

1819

1920
class Dynamic(TypedDict):
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<head>
2+
<link rel="stylesheet"
3+
href="https://cdnjs.cloudflare.com/ajax/libs/modern-normalize/3.0.1/modern-normalize.min.css" />
4+
<style type="text/css">
5+
body {
6+
font-family: "LXGW ZhenKai GB", "LXGW WenKai GB", sans-serif;
7+
}
8+
9+
p {
10+
margin: 0;
11+
font-weight: 400;
12+
}
13+
14+
.author {
15+
padding: 3.2vmin;
16+
display: flex;
17+
align-items: center;
18+
height: 19.2vmin;
19+
}
20+
21+
.avatar {
22+
width: 10.66667vmin;
23+
height: 10.66667vmin;
24+
margin-right: 3.2vmin;
25+
flex-shrink: 0;
26+
border-radius: 9999px;
27+
}
28+
29+
.info {
30+
flex: 1;
31+
}
32+
33+
.name {
34+
color: rgb(251, 114, 153);
35+
font-size: 4vmin;
36+
}
37+
38+
.time {
39+
margin-top: 1px;
40+
font-size: 3.2vmin;
41+
line-height: 4.53333vmin;
42+
color: #9499a0;
43+
}
44+
45+
.content {
46+
color: #18191c;
47+
word-break: break-all;
48+
letter-spacing: 0;
49+
word-wrap: break-word;
50+
font-size: 4.53333vmin;
51+
line-height: 7.73333vmin;
52+
white-space: pre-line;
53+
padding: 0 3.2vmin;
54+
margin-bottom: 3.2vmin;
55+
}
56+
57+
58+
.emoji {
59+
padding: 0 1px;
60+
}
61+
62+
.emoji img {
63+
width: 12.26667vmin;
64+
height: 12.26667vmin;
65+
vertical-align: -.24em;
66+
}
67+
</style>
68+
</head>
69+
70+
<body>
71+
<div class="author">
72+
<img class="avatar" src="{{ dynamic['modules']['module_author']['face'] }}" />
73+
<div class="info">
74+
<div class="name">
75+
{{ dynamic["modules"]["module_author"]["name"] }}
76+
</div>
77+
<div class="time">
78+
{{ dynamic["modules"]["module_author"]["pub_time"] }}
79+
</div>
80+
</div>
81+
</div>
82+
<p class="content">
83+
{%- for node in dynamic["modules"]["module_dynamic"]["major"]["opus"]["summary"]["rich_text_nodes"] -%}
84+
{%- if node["type"] == "RICH_TEXT_NODE_TYPE_TEXT" -%}
85+
<span>{{ node["text"] }}</span>
86+
{%- elif node["type"] == "RICH_TEXT_NODE_TYPE_EMOJI" -%}
87+
<span class="emoji"><img src="{{ node['emoji']['icon_url'] }}" /></span>
88+
{%- endif -%}
89+
{%- endfor -%}
90+
</p>
91+
</body>

src/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from inspect import iscoroutine
44
from typing import TYPE_CHECKING, Awaitable, TypeVar, cast
55

6+
from httpx import AsyncClient
67
from nonebot_plugin_alconna import AtAll, SupportAdapter, UniMessage
78
from nonebot_plugin_alconna.uniseg import Receipt
89
from nonebot_plugin_uninfo.orm import SceneModel, get_bot_model
@@ -55,3 +56,19 @@ async def send_message(scene_model: SceneModel, msg: UniMessage) -> Receipt:
5556

5657
def with_prefix(prefix: str) -> Callable[[str], str]:
5758
return lambda name: f"{prefix}_{name}"
59+
60+
61+
client = AsyncClient(
62+
headers={
63+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
64+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"
65+
}
66+
)
67+
68+
69+
async def img_fetch_fn(url: str) -> bytes:
70+
return (await client.get(url)).content
71+
72+
73+
async def css_fetch_fn(url: str) -> str:
74+
return (await client.get(url)).text

0 commit comments

Comments
 (0)