Metadata-Version: 2.4
Name: pixivkit
Version: 0.1.0
Summary: Modern Pixiv API client for Python: App API (OAuth) + Web API (PHPSESSID), direct/proxy only, no DoH.
Author: PixivKit contributors
License-Expression: Unlicense
Project-URL: Homepage, https://github.com/zhy201810576/PixivKit
Project-URL: Repository, https://github.com/zhy201810576/PixivKit
Project-URL: Issues, https://github.com/zhy201810576/PixivKit/issues
Keywords: pixiv,api,pixivapi,pixivkit,app-api,web-api
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Requires-Dist: beautifulsoup4>=4.12.0
Provides-Extra: dev
Requires-Dist: pytest>=7.3; extra == "dev"
Requires-Dist: ruff>=0.4.0; extra == "dev"
Provides-Extra: examples
Requires-Dist: selenium>=4.10.0; extra == "examples"
Dynamic: license-file

# PixivKit

> 一个现代化、零依赖绕过的 Pixiv API Python 客户端 —— 同时提供 App API（OAuth）与 Web API（PHPSESSID）两套完整接口。

[![Python](https://img.shields.io/badge/Python-3.9%2B-blue.svg)](https://www.python.org/)
[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg)](https://unlicense.org/)
[![Dependencies](https://img.shields.io/badge/deps-requests%20%7C%20beautifulsoup4-green.svg)](https://github.com/psf/requests)

---

## 项目简介

PixivKit 是面向开发者的 Pixiv API 客户端库，在 [pixivpy](https://github.com/upbit/pixivpy) 与
[ProjectU](https://github.com/sunbeams001/ProjectU) 的基础上从零重写，清理了历史包袱并补齐了
Web 接口，提供一套干净、类型友好、开箱即用的 Python 接口。

- **App API（OAuth）** —— 继承自 [pixivpy](https://github.com/upbit/pixivpy) 的 `app-api.pixiv.net` 接口面，经过更新与清理。
- **Web API（PHPSESSID）** —— 移植自 [ProjectU](https://github.com/sunbeams001/ProjectU) 的 `www.pixiv.net` 接口面，共 13 个模块、78 个接口。

### ✨ 核心特性

- **无 DoH、无 SNI 绕过** —— 请求直接访问目标主机，或通过你显式配置的代理，不含任何 Cloudflare 规避层。
- **双 API 面** —— `AppPixivAPI`（OAuth 应用 API）+ `WebPixivAPI`（Cookie 网页 API）一次装齐。
- **轻量返回** —— JSON 统一解析为 `JsonDict`（`dict` 子类），支持属性访问与下标访问两种写法。
- **极简依赖** —— 运行时仅 `requests` 与 `beautifulsoup4`，后者只用于 Pixivision / 阅读标记等 HTML 解析。
- **CSRF 自动维护** —— Web API 的 POST 请求自动抓取并刷新 CSRF token，遇到 403 自动重试。
- **开箱即用** —— 内置登录、翻页、图片下载等便捷方法，附带凭据获取示例脚本。

---

## 安装

```bash
pip install -e .
```

> **注意**：需要 Python 3.9 及以上版本。依赖会自动安装 `requests` 与 `beautifulsoup4`。

---

## 快速开始

### App API（OAuth 登录）

```python
from pixivkit import AppPixivAPI

api = AppPixivAPI()
api.login("username", "password")          # 或 api.auth(refresh_token="...")

illusts = api.illust_ranking(mode="day")   # 属性访问
print(illusts.illusts[0].title)

next_qs = api.parse_qs(illusts.next_url)   # 翻页
illusts = api.illust_ranking(**next_qs)

api.download(illusts.illusts[0].image_urls.large)   # 下载原图
```

### Web API（PHPSESSID）

1. 在浏览器中登录 <https://www.pixiv.net>。
2. 复制 `PHPSESSID` Cookie 值（形如 `12345678_abcdef...`）。

```python
from pixivkit import WebPixivAPI

api = WebPixivAPI("12345678_abcdef...")

result = api.illust.get_detail(102814610)
print(result.error, result.message)
print(result.body.title)

page = api.user.get_user_illusts_by_tag(16208053, "女の子")
print(page.body.total)

ranking = api.ranking.get_illust_ranking()  # 原始 JSON
print(ranking.contents[:3])
```

### 获取凭据（examples）

仓库 `examples/` 目录提供两个凭据获取脚本（参考 PyPixAuth 的浏览器授权流程），依赖 `selenium`（可选依赖），先安装：

```bash
pip install "pixivkit[examples]"
```

| 脚本 | 说明 |
| --- | --- |
| `get_refresh_token.py` | Selenium 浏览器登录 + PKCE 授权码换取 App API 的 `refresh_token` |
| `get_phpsessid.py` | Selenium 浏览器登录后从 Cookie 提取 Web API 的 `PHPSESSID` |

```bash
# 获取 refresh_token（弹出浏览器，人工完成登录/验证码后自动捕获授权码）
python examples/get_refresh_token.py --save tokens.json

# 获取 PHPSESSID（弹出浏览器，人工登录后自动从 Cookie 提取）
python examples/get_phpsessid.py --save phpsessid.json

# 指定本地 Chrome / ChromeDriver
python examples/get_refresh_token.py --chrome "C:/Chrome/chrome.exe" --chromedriver "C:/Chrome/chromedriver.exe"
```

拿到凭据后，`refresh_token` 用于 `AppPixivAPI().auth(refresh_token=...)`，`PHPSESSID` 用于 `WebPixivAPI(...)`。

---

## 数据模型：JsonDict

所有 JSON 响应都被解析为 `JsonDict` —— 一个同时支持**属性访问**与**下标访问**的 `dict` 子类，嵌套对象会递归转换。

```python
from pixivkit import JsonDict, parse_json

data = parse_json('{"body": {"title": "hello"}}')
print(data.body.title)          # hello
print(data["body"]["title"])    # hello
print(isinstance(data, dict))   # True
```

> 注意：访问不存在的键会抛出 `AttributeError`（而不是返回 `None`），与普通 Python 对象语义一致。

---

## App API（OAuth）

`AppPixivAPI` 继承自 `BaseClient`，构造参数透传给 `requests`：

```python
AppPixivAPI(
    proxies=None,          # requests 风格的代理映射
    timeout=30.0,          # 默认请求超时（秒）
    headers=None,          # 附加请求头
    verify=True,           # TLS 证书校验（可传 CA 路径或 False）
    **requests_kwargs,     # 其余透传给 requests
)
```

### 认证

```python
api = AppPixivAPI()

# 方式一：账号密码登录
api.login("username", "password")

# 方式二：使用已保存的 refresh_token（推荐）
api.auth(refresh_token="your_refresh_token")

# 方式三：手动注入已有的 access_token
api.set_auth("access_token", "refresh_token")

# 读取登录后的凭据
print(api.user_id)         # 登录用户 ID
print(api.access_token)    # 短期令牌
print(api.refresh_token)   # 长期令牌
```

### App API 接口清单

所有方法均返回 `JsonDict`（或 `JsonDict` 列表），绝大多数带有 `req_auth=True` 参数，可设为 `False` 以无需认证调用。

#### 用户（Users）

| 方法 | 说明 |
| --- | --- |
| `user_detail(user_id)` | 用户详情 |
| `user_illusts(user_id, type="illust")` | 用户的插画 / 漫画作品 |
| `user_novels(user_id)` | 用户的小说 |
| `user_bookmarks_illust(user_id)` | 用户收藏的插画 |
| `user_bookmarks_novel(user_id)` | 用户收藏的小说 |
| `user_related(seed_user_id)` | 相关用户推荐 |
| `user_recommended()` | 推荐用户 |
| `user_following(user_id)` | 该用户关注的用户 |
| `user_follower(user_id)` | 该用户的粉丝 |
| `user_mypixiv(user_id)` | 「我的 Pixiv」（互关）用户 |
| `user_list(user_id)` | 黑名单用户列表 |
| `user_bookmark_tags_illust(user_id)` | 用户插画收藏标签 |

#### 插画（Illustrations）

| 方法 | 说明 |
| --- | --- |
| `illust_follow()` | 已关注用户的最新插画 |
| `illust_detail(illust_id)` | 插画详情 |
| `illust_comments(illust_id)` | 插画评论 |
| `illust_related(illust_id)` | 相关插画推荐 |
| `illust_recommended()` | 推荐插画（首页） |
| `illust_new()` | 全站最新插画 |
| `ugoira_metadata(illust_id)` | 动图（ugoira）元数据 |

#### 小说（Novels）

| 方法 | 说明 |
| --- | --- |
| `novel_detail(novel_id)` | 小说详情（直接返回 `novel` 对象） |
| `novel_comments(novel_id)` | 小说评论 |
| `novel_recommended()` | 推荐小说 |
| `novel_new()` | 全站最新小说 |
| `novel_follow()` | 已关注用户的最新小说 |
| `novel_series(series_id)` | 小说系列详情 |
| `webview_novel(novel_id)` | 通过 webview 获取小说正文（`raw=True` 返回 HTML） |
| `novel_text(novel_id)` | 小说正文（`webview_novel` 的别名） |

#### 排行 / 搜索（Ranking / Search）

| 方法 | 说明 |
| --- | --- |
| `illust_ranking(mode="day")` | 插画排行榜 |
| `trending_tags_illust()` | 插画热门标签 |
| `search_illust(word)` | 搜索插画 |
| `search_novel(word)` | 搜索小说 |
| `search_user(word)` | 搜索用户 |

#### 收藏 / 关注 / 其他（Bookmarks / Follows / Misc）

| 方法 | 说明 |
| --- | --- |
| `illust_bookmark_detail(illust_id)` | 插画收藏详情 |
| `illust_bookmark_add(illust_id, restrict="public", tags=None)` | 收藏插画 |
| `illust_bookmark_delete(illust_id)` | 取消收藏插画 |
| `user_follow_add(user_id, restrict="public")` | 关注用户 |
| `user_follow_delete(user_id)` | 取消关注用户 |
| `user_edit_ai_show_settings(setting)` | 开关 AI 作品显示 |
| `showcase_article(showcase_id)` | 特别企划文章（走公开 Web 接口） |

### App API 调用与返回示例

#### 插画排行榜

```python
illusts = api.illust_ranking(mode="day", date="2024-01-01")
print(illusts.illusts[0].title)
```

返回数据示例（节选）：

```json
{
  "illusts": [
    {
      "id": 102814610,
      "title": "イラストタイトル",
      "type": "illust",
      "image_urls": {
        "square_medium": "https://i.pximg.net/c/360x360_70/img-master/img/..._square1200.jpg",
        "medium": "https://i.pximg.net/c/540x540_70/img-master/img/..._master1200.jpg",
        "large": "https://i.pximg.net/img-master/img/..._master1200.jpg"
      },
      "caption": "説明文",
      "restrict": 0,
      "user": {
        "id": 16208053,
        "name": "ユーザー名",
        "account": "user_account",
        "profile_image_urls": {
          "medium": "https://i.pximg.net/user-profile/img/..._50.jpg"
        },
        "is_followed": false
      },
      "tags": [
        { "name": "オリジナル", "translated_name": "原创" }
      ],
      "create_date": "2024-01-01T00:00:00+09:00",
      "page_count": 1,
      "width": 1200,
      "height": 900,
      "total_view": 12345,
      "total_bookmarks": 678,
      "is_bookmarked": false,
      "illust_ai_type": 1,
      "illust_book_style": 0
    }
  ],
  "next_url": "https://app-api.pixiv.net/v1/illust/ranking?mode=day&offset=30",
  "search_span_limit": 315360000
}
```

翻页：

```python
next_qs = api.parse_qs(illusts.next_url)
# {'mode': 'day', 'offset': '30'}
illusts = api.illust_ranking(**next_qs)
```

#### 搜索插画

```python
result = api.search_illust("女の子", sort="date_desc")
for illust in result.illusts:
    print(illust.id, illust.title, illust.total_bookmarks)
```

#### 收藏插画

```python
# 公开收藏
api.illust_bookmark_add(102814610, restrict="public", tags=["かわいい", "女の子"])
# 私密收藏
api.illust_bookmark_add(102814610, restrict="private")
```

#### 小说正文

```python
novel = api.novel_detail(1234567)
print(novel.title)

text = api.novel_text(1234567)   # 返回 {..., "novel_text": "正文"}
print(text.novel_text)
```

---

## Web API（PHPSESSID）

### 初始化与通用能力

```python
api = WebPixivAPI(
    php_session_id="12345678_abcdef...",   # 必填：PHPSESSID Cookie 值
    token=None,        # 可选 CSRF token，缺省时自动获取
    host="https://www.pixiv.net",          # 默认主机
    lang="zh",         # 每个请求自动追加的 lang 参数
    proxies=None,      # requests 风格代理映射
    timeout=30.0,      # 默认超时
    headers=None,      # 附加请求头
)
```

便捷属性与方法：

```python
print(api.user_id)      # 从 PHPSESSID 解析出的用户 ID（int）

# 热切换 PHPSESSID 而无需重建 facade
api.set_php_session_id("99999999_newtoken", reset_csrf=True)

# 获取（必要时自动抓取）CSRF token
token = api.get_token()

# 下载图片（复用会话与代理，自动附带 Referer）
api.download("https://i.pximg.net/img-master/img/..._master1200.jpg")
```

> Web API 采用 Cookie 认证：传入浏览器登录后的 `PHPSESSID` 即可。POST 请求所需的 CSRF token 会自动从账户设置页抓取并缓存，遇到 403 会自动刷新重试。

### Web API 模块与接口清单

13 个模块通过 facade 属性访问：

| 属性 | 模块 | 接口数 | 说明 |
| --- | --- | --- | --- |
| `api.illust` | `IllustApi` | 8 | 插画详情、分页、动图、发现、推荐、点赞 |
| `api.illust_series` | `IllustSeriesApi` | 3 | 漫画系列详情、追更/取消追更 |
| `api.user` | `UserApi` | 16 | 用户资料、关注/粉丝、按标签筛选作品 |
| `api.bookmark` | `BookmarkApi` | 10 | 插画/小说收藏的增删查 |
| `api.ranking` | `RankingApi` | 2 | 插画/小说排行榜 |
| `api.comment` | `CommentApi` | 8 | 插画/小说评论的读写 |
| `api.novel` | `NovelApi` | 5 | 小说详情、发现、推荐 |
| `api.novel_series` | `NovelSeriesApi` | 5 | 小说系列详情、章节、追更 |
| `api.tag` | `TagApi` | 8 | 标签联想、信息、增删 |
| `api.marker` | `MarkerApi` | 4 | 小说「稍后阅读」标记 |
| `api.follow` | `FollowApi` | 4 | 关注用户最新作品、追更列表 |
| `api.search` | `SearchApi` | 3 | 插画/小说/用户搜索 |
| `api.pixivision` | `PixivisionApi` | 2 | Pixivision 特辑文章 |

#### `api.illust` — 插画

| 方法 | 说明 |
| --- | --- |
| `get_detail(pid)` | 插画详情 |
| `get_bookmark_data(pid)` | 插画收藏状态 |
| `get_pages(pid)` | 多页插画的所有图片 URL |
| `get_ugoira_meta(pid)` | 动图元数据 |
| `get_discovery(mode="all", limit=100)` | 编辑精选「发现」插画 |
| `get_recommend_init(pid, limit=18)` | 以某插画为种子初始化推荐 |
| `get_recommend_illusts(illust_ids)` | 基于多个种子 ID 的推荐 |
| `like(pid)` | 点赞插画 |

#### `api.illust_series` — 漫画系列

| 方法 | 说明 |
| --- | --- |
| `get_detail(series_id, page=1)` | 系列详情 |
| `watch(series_id)` | 追更系列 |
| `unwatch(series_id)` | 取消追更系列 |

#### `api.user` — 用户

| 方法 | 说明 |
| --- | --- |
| `get_user_info(uid, full=1)` | 用户信息 |
| `get_profile_all(uid)` | 用户主页汇总（作品、收藏、统计） |
| `get_profile_illusts(uid, ids)` | 按作品 ID 列表取插画详情 |
| `get_profile_novels(uid, ids)` | 按小说 ID 列表取小说详情 |
| `get_user_following(uid)` | 该用户的关注列表 |
| `get_user_followers(uid)` | 该用户的粉丝列表 |
| `get_recommend_users(uid)` | 基于某用户的推荐用户 |
| `get_discovery_users(limit=20)` | 为当前账号推荐的用户 |
| `get_user_follow_detail(user_id)` | 关注状态详情 |
| `follow_user(user_id, tag="", restrict=0)` | 关注用户 |
| `unfollow_user(user_id)` | 取消关注用户 |
| `get_my_pixiv(uid)` | 「我的 Pixiv」（互关）用户 |
| `get_user_illust_tags(uid, all=1)` | 用户的全部插画标签 |
| `get_user_illusts_by_tag(uid, tag)` | 按标签筛选用户插画 |
| `get_user_novel_tags(uid, all=1)` | 用户的全部小说标签 |
| `get_user_novels_by_tag(uid, tag)` | 按标签筛选用户小说 |

#### `api.bookmark` — 收藏

| 方法 | 说明 |
| --- | --- |
| `get_user_bookmark_illusts(uid, tag="", offset=0, limit=48)` | 用户收藏的插画 |
| `get_user_bookmark_novels(uid, tag="", offset=0, limit=30)` | 用户收藏的小说 |
| `get_illust_bookmark_tags(user_id)` | 用户的插画收藏标签 |
| `get_novel_bookmark_tags(user_id)` | 用户的小说收藏标签 |
| `add_illust(illust_id, restrict=0, comment="", tags=None)` | 收藏插画 |
| `delete_illust(bookmark_id)` | 删除单个插画收藏 |
| `delete_illusts(bookmark_ids)` | 批量删除插画收藏 |
| `add_novel(novel_id, restrict=0, comment="", tags=None)` | 收藏小说（返回收藏 ID） |
| `delete_novel(book_id)` | 删除单个小说收藏 |
| `delete_novels(bookmark_ids)` | 批量删除小说收藏 |

#### `api.ranking` — 排行榜

| 方法 | 说明 |
| --- | --- |
| `get_illust_ranking(mode=RankingMode.DAILY, page=1, content=RankingContent.ALL, date=None)` | 插画排行榜（**原始 JSON**，非包裹格式） |
| `get_novel_ranking_json(mode=RankingMode.DAILY, page=1, content=RankingContent.NOVEL, date=None)` | 小说排行榜（标准 `PixivResponse` 包裹格式） |

#### `api.comment` — 评论

| 方法 | 说明 |
| --- | --- |
| `get_illust_comment_roots(illust_id, offset=0, limit=20)` | 插画根评论 |
| `get_comment_replies(comment_id, page=1)` | 插画评论的回复 |
| `post_illust_comment(illust_id, user_id, comment=None, stamp_id=None)` | 发表插画评论（文字或贴图） |
| `delete_illust_comment(illust_id, comment_id)` | 删除插画评论 |
| `get_novel_comment_roots(novel_id, offset=0, limit=20)` | 小说根评论 |
| `get_novel_comment_replies(comment_id, page=1)` | 小说评论的回复 |
| `post_novel_comment(novel_id, user_id, comment=None, stamp_id=None)` | 发表小说评论 |
| `delete_novel_comment(novel_id, comment_id)` | 删除小说评论 |

#### `api.novel` — 小说

| 方法 | 说明 |
| --- | --- |
| `get_detail(novel_id)` | 小说详情 |
| `get_bookmark_data(novel_id)` | 小说收藏状态 |
| `get_discovery(mode="all", limit=100)` | 编辑精选「发现」小说 |
| `get_recommend_init(novel_id, limit=9)` | 以某小说为种子初始化推荐 |
| `get_recommend_novels(novel_ids)` | 基于多个种子 ID 的推荐 |

#### `api.novel_series` — 小说系列

| 方法 | 说明 |
| --- | --- |
| `get_detail(series_id)` | 系列详情 |
| `get_contents(series_id, limit=30, last_order=None, order_by="asc")` | 系列内作品列表 |
| `get_titles(series_id)` | 系列章节标题 |
| `watch(series_id)` | 追更系列 |
| `unwatch(series_id)` | 取消追更系列 |

#### `api.tag` — 标签

| 方法 | 说明 |
| --- | --- |
| `get_suggest_by_word(keyword)` | 关键词标签候选（ajax） |
| `get_search_recommendations(mode="all")` | 搜索框推荐内容（热门/推荐标签） |
| `search_tag_autocomplete(keyword)` | 标签实时联想（RPC，原始响应） |
| `get_tag_info(tag, lang=None)` | 标签信息与翻译 |
| `add_illust_tag(illust_id, tag)` | 给插画添加自定义标签 |
| `delete_illust_tag(illust_id, tag)` | 删除插画自定义标签 |
| `add_novel_tag(novel_id, tag)` | 给小说添加自定义标签 |
| `delete_novel_tag(novel_id, tag)` | 删除小说自定义标签 |

#### `api.marker` — 小说阅读标记

| 方法 | 说明 |
| --- | --- |
| `add_novel_marker(novel_id, user_id, page=1)` | 添加小说「稍后阅读」标记 |
| `delete_novel_marker(novel_id, user_id)` | 移除阅读标记 |
| `get_novel_marker_list()` | 全部阅读标记（从 HTML 解析） |
| `delete_novel_marker_by_id(marker_id)` | 按标记 ID 删除（返回结果 HTML） |

#### `api.follow` — 关注动态

| 方法 | 说明 |
| --- | --- |
| `get_follow_latest_illust(mode="all", page=1)` | 关注用户的最新插画 |
| `get_follow_latest_novel(mode="all", page=1)` | 关注用户的最新小说 |
| `get_watch_list_manga(page=1)` | 追更的漫画系列列表 |
| `get_watch_list_novel(page=1)` | 追更的小说系列列表 |

#### `api.search` — 搜索

| 方法 | 说明 |
| --- | --- |
| `search_illust(keyword, search_mode="s_tag", order="date_d", mode="all", page=1)` | 搜索插画（含漫画/动图） |
| `search_novel(keyword, search_mode="s_tag", order="date_d", mode="all", page=1)` | 搜索小说 |
| `search_user(keyword, search_mode="s_usr", has_work=1, page=1)` | 按昵称搜索用户 |

#### `api.pixivision` — Pixivision 特辑

| 方法 | 说明 |
| --- | --- |
| `get_article_list(category="illustration", lang="zh", page=1)` | 特辑文章列表 |
| `get_article_detail(article_id, lang="zh")` | 特辑文章详情（含嵌入作品） |

### Web API 调用与返回示例

#### 插画详情

```python
result = api.illust.get_detail(102814610)
print(result.error, result.message)   # False ""
print(result.body.title)
print(result.body.urls.original)      # 原图 URL
```

返回数据示例（节选）：

```json
{
  "error": false,
  "message": "",
  "body": {
    "illustId": "102814610",
    "illustTitle": "イラストタイトル",
    "illustType": 0,
    "createDate": "2024-01-01T00:00:00+09:00",
    "restrict": 0,
    "xRestrict": 0,
    "urls": {
      "mini": "https://i.pximg.net/c/48x48/img-master/img/..._square1200.jpg",
      "thumb": "https://i.pximg.net/c/250x250_80_a2/img-master/img/..._square1200.jpg",
      "small": "https://i.pximg.net/c/540x540_70/img-master/img/..._master1200.jpg",
      "regular": "https://i.pximg.net/img-master/img/..._master1200.jpg",
      "original": "https://i.pximg.net/img-original/img/..._p0.jpg"
    },
    "tags": {
      "authorId": "16208053",
      "isLocked": false,
      "tags": [
        { "tag": "オリジナル", "translation": { "en": "original" } }
      ]
    },
    "userId": "16208053",
    "userName": "ユーザー名",
    "userAccount": "user_account",
    "width": 1200,
    "height": 900,
    "pageCount": 1,
    "bookmarkCount": 678,
    "likeCount": 12345,
    "commentCount": 0,
    "viewCount": 12345,
    "aiType": 1
  }
}
```

#### 排行榜（原始 JSON）

```python
ranking = api.ranking.get_illust_ranking(mode="daily", page=1)
print(ranking.contents[:3])
```

返回数据示例（节选）：

```json
{
  "contents": [
    {
      "title": "イラストタイトル",
      "date": "20240101",
      "tags": ["オリジナル", "女の子"],
      "url": "https://www.pixiv.net/artworks/102814610",
      "illust_type": "0",
      "illust_page_count": "1",
      "user_name": "ユーザー名",
      "profile_img": "https://i.pximg.net/user-profile/img/..._50.png",
      "illust_id": "102814610",
      "width": "1200",
      "height": "900",
      "user_id": "16208053",
      "rank": 1,
      "total_view": "12345",
      "total_bookmarks": "678"
    }
  ],
  "mode": "daily",
  "content": "all",
  "page": 1,
  "prev": false,
  "next": 2,
  "date": "20240101",
  "prev_date": "20231231",
  "next_date": false,
  "rank_total": 50
}
```

#### 搜索插画

```python
result = api.search.search_illust("女の子", mode="all", page=1)
for illust in result.body.illustManga.data:
    print(illust.id, illust.title)
```

#### 收藏与取消收藏

```python
# 收藏插画（restrict: 0 公开 / 1 私密）
api.bookmark.add_illust(102814610, restrict=0, tags=["かわいい"])

# 批量删除收藏
api.bookmark.delete_illusts(["12345678", "87654321"])
```

#### 小说阅读标记

```python
# 添加「稍后阅读」标记
api.marker.add_novel_marker(1234567, 16208053, page=1)

# 列出全部标记（返回 JsonDict，含 total 与 novels）
markers = api.marker.get_novel_marker_list()
print(markers.total)
print(markers.novels[0].title)
```

返回数据示例：

```json
{
  "total": 3,
  "novels": [
    {
      "id": "1234567",
      "title": "小説タイトル",
      "user_id": "16208053",
      "user_name": "作者名",
      "cover_url": "https://i.pximg.net/c/240x480_80_a2/novel-cover/...",
      "text_count": 12000,
      "bookmark_count": 345,
      "tags": ["オリジナル", "ファンタジー"],
      "description": "あらすじ",
      "x_restrict": 0,
      "series_id": "98765",
      "series_title": "シリーズ名"
    }
  ]
}
```

#### Pixivision 特辑

```python
articles = api.pixivision.get_article_list(category="illustration", lang="zh")
print(articles.articles[0].title)

detail = api.pixivision.get_article_detail(12345, lang="zh")
for artwork in detail.artworks:
    print(artwork.artwork_id, artwork.artwork_title, artwork.author_name)
```

---

## 排行榜模式枚举

排行榜相关枚举从 `pixivkit` 直接导入，带有二维分类（分类 × AI）元数据：

```python
from pixivkit import (
    RankingMode, RankingContent, RankingCategory, RankingAiType,
)
from pixivkit.enums import get_supported_modes, is_compatible

# 安全（非 R18、非 AI）模式
RankingMode.get_general_non_ai_modes()
# R-18 AI 模式
RankingMode.get_r18_ai_modes()
# 按分类 + AI 组合过滤
RankingMode.get_modes(RankingCategory.GENERAL, RankingAiType.NON_AI)
# 仅小说排行榜支持的模式：weekly_original, weekly_ai, weekly_r18_ai
RankingMode.get_novel_exclusive_modes()

# 内容类型与模式的兼容性
get_supported_modes(RankingContent.NOVEL)
is_compatible(RankingContent.ILLUST, RankingMode.DAILY)   # True
```

`RankingMode` 成员与值对照：

| 成员 | 值 | 分类 | AI |
| --- | --- | --- | --- |
| `DAILY` | `daily` | general | 否 |
| `WEEKLY` | `weekly` | general | 否 |
| `MONTHLY` | `monthly` | general | 否 |
| `ROOKIE` | `rookie` | general | 否 |
| `ORIGINAL` | `original` | general | 否 |
| `MALE` / `FEMALE` | `male` / `female` | general | 否 |
| `WEEKLY_ORIGINAL` | `weekly_original` | general | 否（仅小说） |
| `DAILY_AI` | `daily_ai` | general | 是 |
| `WEEKLY_AI` | `weekly_ai` | general | 是（仅小说） |
| `DAILY_R18` | `daily_r18` | r18 | 否 |
| `WEEKLY_R18` | `weekly_r18` | r18 | 否 |
| `MALE_R18` / `FEMALE_R18` | `male_r18` / `female_r18` | r18 | 否 |
| `DAILY_R18_AI` | `daily_r18_ai` | r18 | 是 |
| `WEEKLY_R18_AI` | `weekly_r18_ai` | r18 | 是（仅小说） |
| `R18G` | `r18g` | r18g | 否 |

`RankingApi` 同时接受 `RankingMode` 成员或纯字符串：

```python
api.ranking.get_illust_ranking(mode=RankingMode.WEEKLY)
api.ranking.get_illust_ranking(mode="weekly_r18")
```

---

## 异常处理

PixivKit 的失败统一抛出 `PixivError`，携带 `reason`、`header`、`body` 三个属性：

```python
from pixivkit import AppPixivAPI, PixivError

api = AppPixivAPI()
try:
    api.login("username", "wrong_password")
except PixivError as exc:
    print(exc.reason)    # 人类可读的失败原因
    print(exc.header)    # HTTP 响应头（如有）
    print(exc.body)      # 原始响应体（如有）
```

> 认证失败时 `PixivError` 会附带服务端返回的响应头与响应体，便于排查账号密码错误或风控/验证码等问题。

---

## 代理配置

两个客户端都接受标准 `requests` 代理映射：

```python
from pixivkit import AppPixivAPI, WebPixivAPI

proxies = {
    "http": "http://127.0.0.1:7890",
    "https": "http://127.0.0.1:7890",
}

app = AppPixivAPI(proxies=proxies)
web = WebPixivAPI("PHPSESSID", proxies=proxies)

# 也可在运行时替换代理
app.set_proxies({"http": "socks5://127.0.0.1:1080", "https": "socks5://127.0.0.1:1080"})
```

> PixivKit 不做任何 DNS-over-HTTPS 或 SNI 绕过。若目标主机在你的网络下不可达（例如图片 CDN `i.pximg.net`），请通过代理访问，这是唯一受支持的流量路由方式。

---

## 与 pixivpy3 的区别

- 移除了 `ByPassSniApi` 与全部 DoH 逻辑。
- 移除了 `cloudscraper` 与 `requests-toolbelt`，改用纯 `requests`。
- 移除了 `pydantic` 模型，所有响应均为支持属性访问的 `JsonDict`。
- 新增 Web API（`/ajax/*`）支持，并自动刷新 CSRF token。

---

## 参考项目

- [pixivpy](https://github.com/upbit/pixivpy) —— 老牌 Pixiv Python API 客户端，本项目 App API 接口面的主要来源。
- [ProjectU](https://github.com/sunbeams001/ProjectU) —— 开源 Kotlin Multiplatform Pixiv 客户端，本项目 Web API 接口面的主要来源。

---

## License

本项目遵循 [Unlicense](https://unlicense.org/) 协议。
