nonebot-plugin-pixiv-dl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DuoDuoJuZi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-pixiv-dl
3
+ Version: 0.1.0
4
+ Summary: 基于 Pixiv Web AJAX 的 NoneBot2 OneBot V11 搜索与下载插件
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/DuoDuoJuZi/nonebot-plugin-pixiv-dl
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: nonebot2<3,>=2.3
11
+ Requires-Dist: nonebot-adapter-onebot<3,>=2.4
12
+ Requires-Dist: httpx[socks]<0.29,>=0.27
13
+ Requires-Dist: httpcore<2,>=1
14
+ Requires-Dist: pydantic<3,>=2
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: ruff>=0.6; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ <div align="center">
21
+ <img src="https://github.com/Misty02600/nonebot-plugin-template/releases/download/assets/NoneBotPlugin.png" width="310" alt="NoneBot 插件图标">
22
+
23
+ <h1>nonebot-plugin-pixiv-dl</h1>
24
+
25
+ <p>基于 NoneBot2 的 Pixiv 搜索与下载插件</p>
26
+ </div>
27
+
28
+ ## 📖 介绍
29
+
30
+ 通过 Pixiv Web AJAX 搜索插画,漫画和小说,使用 OneBot V11 合并转发发送结果
31
+
32
+ 支持下载插画和漫画原图,以及将小说正文保存为 UTF-8 TXT 文件
33
+
34
+ ## 💿 安装
35
+
36
+ ```bash
37
+ pip install nonebot-plugin-pixiv-dl
38
+ ```
39
+
40
+ <details>
41
+ <summary>从本地源码安装</summary>
42
+
43
+ 在插件目录执行
44
+
45
+ ```bash
46
+ pip install .
47
+ ```
48
+
49
+ 使用 uv 时执行
50
+
51
+ ```bash
52
+ uv pip install .
53
+ ```
54
+
55
+ </details>
56
+
57
+ 在 NoneBot2 项目的 `pyproject.toml` 中,将插件加载名加入 `[tool.nonebot]` 的 `plugins` 列表
58
+
59
+ ```toml
60
+ [tool.nonebot]
61
+ plugins = ["nonebot_plugin_pixiv_dl"]
62
+ ```
63
+
64
+ 已有 `plugins` 配置时只需追加 `nonebot_plugin_pixiv_dl`,同时确保项目已注册 OneBot V11 适配器
65
+
66
+ `nonebot-plugin-pixiv-dl` 是 PyPI 包名,插件加载名为 `nonebot_plugin_pixiv_dl`
67
+
68
+ ## ⚙️ 配置
69
+
70
+ 在机器人项目的 `.env` 文件或环境变量中设置 Pixiv Cookie
71
+
72
+ ```dotenv
73
+ PIXIV_COOKIE="PHPSESSID=替换为自己的会话值"
74
+ PIXIV_PROXY="http://127.0.0.1:7890"
75
+ ```
76
+
77
+ `PIXIV_COOKIE` 只填写浏览器请求中的 Cookie 值,不要包含 `Cookie:` 前缀
78
+
79
+ `PIXIV_PROXY` 为可选配置,可填写可用的 HTTP 或 SOCKS 代理地址
80
+
81
+ | 配置项 | 必填 | 默认值 | 说明 |
82
+ | :--- | :---: | :---: | :--- |
83
+ | `PIXIV_COOKIE` | 是 | 空 | Pixiv Web 登录 Cookie |
84
+ | `PIXIV_R18` | 否 | `true` | 是否允许搜索和下载 R18 作品 |
85
+ | `PIXIV_SEARCH_LIMIT` | 否 | `20` | 每个分类最多返回的搜索结果数 |
86
+ | `PIXIV_FORWARD_MAX_MESSAGES` | 否 | `20` | 每包合并转发的节点上限,最小为 `2` |
87
+ | `PIXIV_DOWNLOAD_MAX_PAGES` | 否 | `30` | 漫画单次最多下载的页数 |
88
+ | `PIXIV_NOVEL_MAX_CHAPTERS` | 否 | `50` | 小说系列单次最多下载的章节数 |
89
+ | `PIXIV_TIMEOUT` | 否 | `20` | 单次网络请求的超时秒数 |
90
+ | `PIXIV_PROXY` | 否 | 空 | Pixiv 网页和图片请求共用的代理地址 |
91
+ | `PIXIV_FIXED_IP` | 否 | 空 | Pixiv 主站使用的固定 IP 地址 |
92
+
93
+ 未配置 `PIXIV_PROXY` 和 `PIXIV_FIXED_IP` 时,插件会读取 `HTTPS_PROXY`,`ALL_PROXY` 等环境代理变量
94
+
95
+ 仅配置 `PIXIV_FIXED_IP` 时,只有 `www.pixiv.net` 的 TCP 连接使用该地址,图片 CDN 仍按原域名连接
96
+
97
+ 同时配置 `PIXIV_PROXY` 和 `PIXIV_FIXED_IP` 时优先使用代理,固定 IP 变化后需要自行更新
98
+
99
+ 插件仅向 `www.pixiv.net` 发送 Cookie,图片请求不会携带 Cookie
100
+
101
+ ## 🎉 使用
102
+
103
+ ### 指令表
104
+
105
+ | 指令 | 范围 | 说明 |
106
+ | :--- | :---: | :--- |
107
+ | `/px搜索图片 关键词` | 群聊和私聊 | 搜索插画并返回元数据 |
108
+ | `/px搜索漫画 关键词` | 群聊和私聊 | 搜索漫画并返回元数据 |
109
+ | `/px搜索小说 关键词` | 群聊和私聊 | 搜索小说并返回元数据 |
110
+ | `/px搜索 关键词` | 群聊和私聊 | 分别返回插画,漫画和小说的搜索结果 |
111
+ | `/px下载图片 PID` | 群聊和私聊 | 下载插画原图并逐页发送 |
112
+ | `/px下载漫画 PID` | 群聊和私聊 | 下载漫画原图并逐页发送 |
113
+ | `/px下载小说 ID` | 群聊和私聊 | 下载独立小说或所在系列的章节 TXT 文件 |
114
+ | `/px下载 PID` | 群聊和私聊 | 查询作品详情后识别插画,漫画或小说 |
115
+
116
+ 指令以 `/px` 开头,分类与参数之间可以省略空格,例如 `/px搜索图片风景` 和 `/px下载图片123456`
117
+
118
+ 搜索结果每个作品占一个合并转发节点,只展示标题,作者,ID,标签等元数据
119
+
120
+ 下载结果以元数据为首节点,插画和漫画每页一张图片,小说每章一个 TXT 文件,超过节点上限会继续分包
121
+
122
+ 搜索或下载时会发送临时状态消息,发送结果时尝试撤回,没有搜索结果时会直接发送提示
123
+
124
+ ### 使用限制
125
+
126
+ - `PIXIV_R18=false` 时,搜索会过滤 R18 作品,下载会拒绝 R18 作品
127
+ - 暂不支持下载 Pixiv 动图
128
+ - 小说文件通过 NapCat 支持的 `file` 扩展消息段发送,实现端需要支持合并转发中的文件消息段,并能访问插件生成的本地临时文件
129
+ - 合并转发依赖实现端支持 `send_group_forward_msg` 或 `send_private_forward_msg` 扩展动作
130
+ - 群聊文件发送还可能受到 QQ 文件容量限制
131
+
132
+ ## 🔧 常见问题
133
+
134
+ - 搜索或下载失败时,查看控制台中的 Pixiv 请求路径,HTTP 状态和异常信息
135
+ - Cookie 缺失或失效时,更新 `PIXIV_COOKIE`
136
+ - 图片下载失败时,检查 `i.pximg.net` 的网络连接或代理设置
137
+ - 固定 IP 无法连接时,清空 `PIXIV_FIXED_IP` 以恢复系统 DNS
138
+ - 小说文件发送失败时,确认实现端支持合并转发中的 `file` 消息段,且能访问本地临时文件
@@ -0,0 +1,119 @@
1
+ <div align="center">
2
+ <img src="https://github.com/Misty02600/nonebot-plugin-template/releases/download/assets/NoneBotPlugin.png" width="310" alt="NoneBot 插件图标">
3
+
4
+ <h1>nonebot-plugin-pixiv-dl</h1>
5
+
6
+ <p>基于 NoneBot2 的 Pixiv 搜索与下载插件</p>
7
+ </div>
8
+
9
+ ## 📖 介绍
10
+
11
+ 通过 Pixiv Web AJAX 搜索插画,漫画和小说,使用 OneBot V11 合并转发发送结果
12
+
13
+ 支持下载插画和漫画原图,以及将小说正文保存为 UTF-8 TXT 文件
14
+
15
+ ## 💿 安装
16
+
17
+ ```bash
18
+ pip install nonebot-plugin-pixiv-dl
19
+ ```
20
+
21
+ <details>
22
+ <summary>从本地源码安装</summary>
23
+
24
+ 在插件目录执行
25
+
26
+ ```bash
27
+ pip install .
28
+ ```
29
+
30
+ 使用 uv 时执行
31
+
32
+ ```bash
33
+ uv pip install .
34
+ ```
35
+
36
+ </details>
37
+
38
+ 在 NoneBot2 项目的 `pyproject.toml` 中,将插件加载名加入 `[tool.nonebot]` 的 `plugins` 列表
39
+
40
+ ```toml
41
+ [tool.nonebot]
42
+ plugins = ["nonebot_plugin_pixiv_dl"]
43
+ ```
44
+
45
+ 已有 `plugins` 配置时只需追加 `nonebot_plugin_pixiv_dl`,同时确保项目已注册 OneBot V11 适配器
46
+
47
+ `nonebot-plugin-pixiv-dl` 是 PyPI 包名,插件加载名为 `nonebot_plugin_pixiv_dl`
48
+
49
+ ## ⚙️ 配置
50
+
51
+ 在机器人项目的 `.env` 文件或环境变量中设置 Pixiv Cookie
52
+
53
+ ```dotenv
54
+ PIXIV_COOKIE="PHPSESSID=替换为自己的会话值"
55
+ PIXIV_PROXY="http://127.0.0.1:7890"
56
+ ```
57
+
58
+ `PIXIV_COOKIE` 只填写浏览器请求中的 Cookie 值,不要包含 `Cookie:` 前缀
59
+
60
+ `PIXIV_PROXY` 为可选配置,可填写可用的 HTTP 或 SOCKS 代理地址
61
+
62
+ | 配置项 | 必填 | 默认值 | 说明 |
63
+ | :--- | :---: | :---: | :--- |
64
+ | `PIXIV_COOKIE` | 是 | 空 | Pixiv Web 登录 Cookie |
65
+ | `PIXIV_R18` | 否 | `true` | 是否允许搜索和下载 R18 作品 |
66
+ | `PIXIV_SEARCH_LIMIT` | 否 | `20` | 每个分类最多返回的搜索结果数 |
67
+ | `PIXIV_FORWARD_MAX_MESSAGES` | 否 | `20` | 每包合并转发的节点上限,最小为 `2` |
68
+ | `PIXIV_DOWNLOAD_MAX_PAGES` | 否 | `30` | 漫画单次最多下载的页数 |
69
+ | `PIXIV_NOVEL_MAX_CHAPTERS` | 否 | `50` | 小说系列单次最多下载的章节数 |
70
+ | `PIXIV_TIMEOUT` | 否 | `20` | 单次网络请求的超时秒数 |
71
+ | `PIXIV_PROXY` | 否 | 空 | Pixiv 网页和图片请求共用的代理地址 |
72
+ | `PIXIV_FIXED_IP` | 否 | 空 | Pixiv 主站使用的固定 IP 地址 |
73
+
74
+ 未配置 `PIXIV_PROXY` 和 `PIXIV_FIXED_IP` 时,插件会读取 `HTTPS_PROXY`,`ALL_PROXY` 等环境代理变量
75
+
76
+ 仅配置 `PIXIV_FIXED_IP` 时,只有 `www.pixiv.net` 的 TCP 连接使用该地址,图片 CDN 仍按原域名连接
77
+
78
+ 同时配置 `PIXIV_PROXY` 和 `PIXIV_FIXED_IP` 时优先使用代理,固定 IP 变化后需要自行更新
79
+
80
+ 插件仅向 `www.pixiv.net` 发送 Cookie,图片请求不会携带 Cookie
81
+
82
+ ## 🎉 使用
83
+
84
+ ### 指令表
85
+
86
+ | 指令 | 范围 | 说明 |
87
+ | :--- | :---: | :--- |
88
+ | `/px搜索图片 关键词` | 群聊和私聊 | 搜索插画并返回元数据 |
89
+ | `/px搜索漫画 关键词` | 群聊和私聊 | 搜索漫画并返回元数据 |
90
+ | `/px搜索小说 关键词` | 群聊和私聊 | 搜索小说并返回元数据 |
91
+ | `/px搜索 关键词` | 群聊和私聊 | 分别返回插画,漫画和小说的搜索结果 |
92
+ | `/px下载图片 PID` | 群聊和私聊 | 下载插画原图并逐页发送 |
93
+ | `/px下载漫画 PID` | 群聊和私聊 | 下载漫画原图并逐页发送 |
94
+ | `/px下载小说 ID` | 群聊和私聊 | 下载独立小说或所在系列的章节 TXT 文件 |
95
+ | `/px下载 PID` | 群聊和私聊 | 查询作品详情后识别插画,漫画或小说 |
96
+
97
+ 指令以 `/px` 开头,分类与参数之间可以省略空格,例如 `/px搜索图片风景` 和 `/px下载图片123456`
98
+
99
+ 搜索结果每个作品占一个合并转发节点,只展示标题,作者,ID,标签等元数据
100
+
101
+ 下载结果以元数据为首节点,插画和漫画每页一张图片,小说每章一个 TXT 文件,超过节点上限会继续分包
102
+
103
+ 搜索或下载时会发送临时状态消息,发送结果时尝试撤回,没有搜索结果时会直接发送提示
104
+
105
+ ### 使用限制
106
+
107
+ - `PIXIV_R18=false` 时,搜索会过滤 R18 作品,下载会拒绝 R18 作品
108
+ - 暂不支持下载 Pixiv 动图
109
+ - 小说文件通过 NapCat 支持的 `file` 扩展消息段发送,实现端需要支持合并转发中的文件消息段,并能访问插件生成的本地临时文件
110
+ - 合并转发依赖实现端支持 `send_group_forward_msg` 或 `send_private_forward_msg` 扩展动作
111
+ - 群聊文件发送还可能受到 QQ 文件容量限制
112
+
113
+ ## 🔧 常见问题
114
+
115
+ - 搜索或下载失败时,查看控制台中的 Pixiv 请求路径,HTTP 状态和异常信息
116
+ - Cookie 缺失或失效时,更新 `PIXIV_COOKIE`
117
+ - 图片下载失败时,检查 `i.pximg.net` 的网络连接或代理设置
118
+ - 固定 IP 无法连接时,清空 `PIXIV_FIXED_IP` 以恢复系统 DNS
119
+ - 小说文件发送失败时,确认实现端支持合并转发中的 `file` 消息段,且能访问本地临时文件
@@ -0,0 +1,13 @@
1
+ from nonebot.plugin import PluginMetadata
2
+
3
+ # @Author: DuoDuoJuZi
4
+ # @Date: 2026-09-29
5
+ __plugin_meta__ = PluginMetadata(
6
+ name="nonebot-plugin-pixiv-dl",
7
+ description="通过 Pixiv Web AJAX 搜索和下载作品",
8
+ usage="/px搜索 关键词 或 /px下载 作品 ID",
9
+ type="application",
10
+ supported_adapters={"~onebot.v11"},
11
+ )
12
+
13
+ from . import commands as commands
@@ -0,0 +1,265 @@
1
+ import asyncio
2
+ import re
3
+ from pathlib import Path
4
+ from tempfile import TemporaryDirectory
5
+
6
+ from nonebot import get_driver, get_plugin_config, logger, on_regex
7
+ from nonebot.adapters.onebot.v11 import Bot, MessageEvent
8
+
9
+ from .config import Config
10
+ from .message import (
11
+ KIND_NAMES,
12
+ build_artwork_forward,
13
+ build_novel_forward,
14
+ build_search_forward,
15
+ format_novel,
16
+ format_series,
17
+ send_forward,
18
+ write_novel_file,
19
+ )
20
+ from .models import Artwork, Novel
21
+ from .pixiv import PixivAPIError, PixivClient, PixivNotFoundError, PixivR18Error
22
+
23
+ SEARCH_RE = re.compile(r"^/px搜索(?!(?:图片|漫画|小说)\s*$)(图片|漫画|小说)?\s*(\S.*?)\s*$")
24
+ DOWNLOAD_RE = re.compile(r"^/px下载(图片|漫画|小说)?\s*(\d+)\s*$")
25
+ KINDS = {"图片": "image", "漫画": "manga", "小说": "novel"}
26
+
27
+ config = get_plugin_config(Config)
28
+ client = PixivClient(config)
29
+ get_driver().on_shutdown(client.close)
30
+
31
+ search_matcher = on_regex(SEARCH_RE.pattern, priority=10, block=True)
32
+ download_matcher = on_regex(DOWNLOAD_RE.pattern, priority=10, block=True)
33
+
34
+
35
+ # @Author: DuoDuoJuZi
36
+ # @Date: 2026-09-29
37
+ def parse_search(text: str) -> tuple[str, str] | None:
38
+ """解析搜索命令,兼容分类与关键词之间省略空格的写法
39
+
40
+ Args:
41
+ text: 用户发送的完整搜索命令文本
42
+
43
+ Returns:
44
+ 搜索分类与非空关键词,命令格式不匹配时返回 None
45
+ """
46
+ match = SEARCH_RE.fullmatch(text)
47
+ if not match:
48
+ return None
49
+ return KINDS.get(match.group(1), "all"), match.group(2).strip()
50
+
51
+
52
+ def parse_download(text: str) -> tuple[str, int] | None:
53
+ """解析下载命令,保留用户指定的分类或自动识别标记
54
+
55
+ Args:
56
+ text: 用户发送的完整下载命令文本
57
+
58
+ Returns:
59
+ 下载分类与作品 ID,命令格式不匹配时返回 None
60
+ """
61
+ match = DOWNLOAD_RE.fullmatch(text)
62
+ if not match:
63
+ return None
64
+ return KINDS.get(match.group(1), "auto"), int(match.group(2))
65
+
66
+
67
+ async def _recall(bot: Bot, status: dict) -> None:
68
+ """撤回处理中提示,撤回失败时记录日志并继续发送结果
69
+
70
+ Args:
71
+ bot: 用于调用 OneBot 消息接口的机器人实例
72
+ status: 发送处理中提示时返回的消息信息,包含待撤回的消息标识
73
+ """
74
+ try:
75
+ await bot.delete_msg(message_id=status["message_id"])
76
+ except Exception:
77
+ logger.warning("Pixiv 状态消息撤回失败", exc_info=True)
78
+
79
+
80
+ async def _search_kind(kind: str, word: str) -> list[Artwork] | list[Novel]:
81
+ """调用指定分类的 Pixiv 搜索接口
82
+
83
+ Args:
84
+ kind: 请求的作品分类
85
+ word: 用于匹配作品标签的搜索关键词
86
+
87
+ Returns:
88
+ 符合配置数量上限的该分类作品预览
89
+
90
+ Raises:
91
+ PixivAuthError: 未配置 Cookie 或 Pixiv 要求重新登录
92
+ PixivNotFoundError: 请求的资源不存在或响应没有业务数据
93
+ PixivNetworkError: 请求超时或网络连接失败
94
+ PixivAPIError: HTTP 请求失败或 Pixiv 返回无效业务数据
95
+ """
96
+ method = {
97
+ "image": client.search_artworks,
98
+ "manga": client.search_manga,
99
+ "novel": client.search_novels,
100
+ }[kind]
101
+ return await method(word, config.pixiv_search_limit)
102
+
103
+
104
+ async def _run_search(bot: Bot, event: MessageEvent, kind: str, word: str) -> None:
105
+ """查询作品预览并撤回状态提示,按分类发送合并转发
106
+
107
+ Args:
108
+ bot: 用于调用 OneBot 消息接口的机器人实例
109
+ event: 触发命令的消息事件,决定回复会话
110
+ kind: 搜索分类,all 表示分别查询插画,漫画和小说
111
+ word: 用于匹配作品标签的搜索关键词
112
+ """
113
+ status = await bot.send(event, "正在搜索")
114
+ recalled = False
115
+ try:
116
+ kinds = ["image", "manga", "novel"] if kind == "all" else [kind]
117
+ results = await asyncio.gather(*(_search_kind(item, word) for item in kinds))
118
+ await _recall(bot, status)
119
+ recalled = True
120
+ if all(not items for items in results):
121
+ await bot.send(event, "没有搜索到相关内容")
122
+ return
123
+ for current, items in zip(kinds, results, strict=True):
124
+ if not items:
125
+ await bot.send(event, f"没有搜索到相关{KIND_NAMES[current]}")
126
+ continue
127
+ packets = build_search_forward(
128
+ items, current, config.pixiv_forward_max_messages, int(bot.self_id)
129
+ )
130
+ for packet in packets:
131
+ await send_forward(bot, event, packet)
132
+ except Exception:
133
+ logger.exception("Pixiv 搜索失败,分类={},关键词={}", kind, word)
134
+ if not recalled:
135
+ await _recall(bot, status)
136
+ await bot.send(event, "搜索失败")
137
+
138
+
139
+ async def _get_download_target(kind: str, work_id: int) -> Artwork | Novel:
140
+ """查询 Pixiv 确定下载资源类型,自动模式下插画不存在时尝试小说
141
+
142
+ Args:
143
+ kind: 下载分类,auto 表示根据接口响应自动识别
144
+ work_id: 待查询作品的 Pixiv ID
145
+
146
+ Returns:
147
+ 可下载的插画或小说详情
148
+
149
+ Raises:
150
+ PixivAuthError: 未配置 Cookie 或 Pixiv 要求重新登录
151
+ PixivNotFoundError: 请求的资源不存在或响应没有业务数据
152
+ PixivNetworkError: 请求超时或网络连接失败
153
+ PixivAPIError: 实际作品分类与指定分类不符或接口数据无效
154
+ PixivR18Error: 作品属于限制级内容且配置禁止下载
155
+ """
156
+ if kind == "novel":
157
+ return await client.get_novel(work_id)
158
+ try:
159
+ artwork = await client.get_illust(work_id)
160
+ except PixivNotFoundError:
161
+ if kind == "auto":
162
+ return await client.get_novel(work_id)
163
+ raise
164
+ if kind != "auto" and artwork.type != kind:
165
+ raise PixivAPIError(f"{work_id}: expected {kind}, got {artwork.type}")
166
+ return artwork
167
+
168
+
169
+ async def _run_download(bot: Bot, event: MessageEvent, kind: str, work_id: int) -> None:
170
+ """下载作品并以元数据首节点打包发送,发送结束后清理临时章节文件
171
+
172
+ Args:
173
+ bot: 用于调用 OneBot 消息接口的机器人实例
174
+ event: 触发命令的消息事件,决定回复会话
175
+ kind: 下载分类,auto 表示先查询作品的实际类型
176
+ work_id: 待查询作品的 Pixiv ID
177
+ """
178
+ status = await bot.send(event, "正在下载")
179
+ recalled = False
180
+ try:
181
+ target = await _get_download_target(kind, work_id)
182
+ if isinstance(target, Novel):
183
+ with TemporaryDirectory(prefix="nonebot-pixiv-") as directory:
184
+ folder = Path(directory)
185
+ if target.series_id:
186
+ series = await client.get_novel_series(
187
+ target.series_id, config.pixiv_novel_max_chapters
188
+ )
189
+ chapters = []
190
+ for item in series.chapters:
191
+ try:
192
+ chapters.append(await client.get_novel(item.id))
193
+ except Exception as exc:
194
+ raise PixivAPIError(
195
+ f"series {series.id}, chapter {item.id}: {exc}"
196
+ ) from exc
197
+ paths = [
198
+ write_novel_file(chapter, folder, index)
199
+ for index, chapter in enumerate(chapters, 1)
200
+ ]
201
+ metadata = format_series(series, len(paths))
202
+ else:
203
+ paths = [write_novel_file(target, folder)]
204
+ metadata = format_novel(target)
205
+ if not paths:
206
+ raise PixivAPIError(f"{work_id}: no novel chapters")
207
+ packets = build_novel_forward(
208
+ metadata, paths, config.pixiv_forward_max_messages, int(bot.self_id)
209
+ )
210
+ for packet in packets:
211
+ await send_forward(bot, event, packet)
212
+ await _recall(bot, status)
213
+ recalled = True
214
+ else:
215
+ urls = await client.get_illust_pages(target.id)
216
+ if target.type == "manga":
217
+ urls = urls[: config.pixiv_download_max_pages]
218
+ images = []
219
+ for page, url in enumerate(urls, 1):
220
+ try:
221
+ images.append(await client.download_image(url))
222
+ except Exception as exc:
223
+ raise PixivAPIError(f"artwork {target.id}, page {page}: {exc}") from exc
224
+ await _recall(bot, status)
225
+ recalled = True
226
+ packets = build_artwork_forward(
227
+ target, images, config.pixiv_forward_max_messages, int(bot.self_id)
228
+ )
229
+ for packet in packets:
230
+ await send_forward(bot, event, packet)
231
+ except PixivR18Error:
232
+ if not recalled:
233
+ await _recall(bot, status)
234
+ await bot.send(event, "已关闭 R18,无法下载该作品")
235
+ except Exception:
236
+ logger.exception("Pixiv 下载失败,分类={},作品 ID={}", kind, work_id)
237
+ if not recalled:
238
+ await _recall(bot, status)
239
+ await bot.send(event, "下载失败")
240
+
241
+
242
+ @search_matcher.handle()
243
+ async def handle_search(bot: Bot, event: MessageEvent) -> None:
244
+ """解析 OneBot 消息并执行匹配的搜索命令
245
+
246
+ Args:
247
+ bot: 用于调用 OneBot 消息接口的机器人实例
248
+ event: 触发命令的消息事件,决定回复会话
249
+ """
250
+ parsed = parse_search(event.get_plaintext())
251
+ if parsed:
252
+ await _run_search(bot, event, *parsed)
253
+
254
+
255
+ @download_matcher.handle()
256
+ async def handle_download(bot: Bot, event: MessageEvent) -> None:
257
+ """解析 OneBot 消息并执行匹配的下载命令
258
+
259
+ Args:
260
+ bot: 用于调用 OneBot 消息接口的机器人实例
261
+ event: 触发命令的消息事件,决定回复会话
262
+ """
263
+ parsed = parse_download(event.get_plaintext())
264
+ if parsed:
265
+ await _run_download(bot, event, *parsed)
@@ -0,0 +1,35 @@
1
+ from ipaddress import ip_address
2
+
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+
6
+ # @Author: DuoDuoJuZi
7
+ # @Date: 2026-09-29
8
+ class Config(BaseModel):
9
+ """保存 Pixiv 请求和 OneBot 消息发送所需的配置"""
10
+
11
+ pixiv_cookie: str = ""
12
+ pixiv_r18: bool = True
13
+ pixiv_search_limit: int = Field(default=20, ge=1, le=200)
14
+ pixiv_forward_max_messages: int = Field(default=20, ge=2, le=100)
15
+ pixiv_download_max_pages: int = Field(default=30, ge=1, le=200)
16
+ pixiv_novel_max_chapters: int = Field(default=50, ge=1, le=500)
17
+ pixiv_timeout: float = Field(default=20.0, gt=0)
18
+ pixiv_proxy: str | None = None
19
+ pixiv_fixed_ip: str | None = None
20
+
21
+ @field_validator("pixiv_fixed_ip")
22
+ @classmethod
23
+ def validate_fixed_ip(cls, value: str | None) -> str | None:
24
+ """校验 pixiv_fixed_ip 字段,仅接受有效的 IPv4 或 IPv6 地址
25
+
26
+ Args:
27
+ value: 配置的固定 IP 地址,空值或空字符串表示关闭固定地址路由
28
+
29
+ Returns:
30
+ 规范化后的 IP 地址,关闭固定地址路由时返回 None
31
+
32
+ Raises:
33
+ ValueError: 非空配置无法解析为有效的 IP 地址
34
+ """
35
+ return str(ip_address(value)) if value else None