A Rust client library and CLI tool for the Pixiv App API (6.x), with full API parity with pixivpy.
- 35+ API endpoints — users, illustrations, novels, search, bookmarks, rankings
- Hybrid response models — typed Rust structs with raw JSON fallback for API change resilience
- Async/await — built on tokio and reqwest
- Concurrent downloads — semaphore-based download manager
- SNI bypass — DNS-over-HTTPS for users behind network restrictions (feature flag)
- CLI tool —
pixiv-dlfor quick searches and downloads from the terminal
[dependencies]
pixiv-client = "1.2"With SNI bypass support (for users behind the Great Firewall):
[dependencies]
pixiv-client = { version = "1.1", features = ["gfw-bypass"] }From GitHub Releases (no Rust required):
Download the latest binary for your platform from Releases.
| Platform | File |
|---|---|
| Linux x86_64 | pixiv-dl-*-x86_64-unknown-linux-gnu.tar.gz |
| Linux ARM64 | pixiv-dl-*-aarch64-unknown-linux-gnu.tar.gz |
| macOS Intel | pixiv-dl-*-x86_64-apple-darwin.tar.gz |
| macOS Apple Silicon | pixiv-dl-*-aarch64-apple-darwin.tar.gz |
| Windows x86_64 | pixiv-dl-*-x86_64-pc-windows-msvc.zip |
# Linux / macOS
tar xzf pixiv-dl-*.tar.gz
chmod +x pixiv-dl
sudo mv pixiv-dl /usr/local/bin/ # or anywhere in your PATH
# Windows — extract the zip and add the folder to your PATHFrom source (requires Rust):
cargo install --path pixiv-dluse pixiv_client::PixivApi;
#[tokio::main]
async fn main() -> pixiv_client::Result<()> {
let api = PixivApi::new();
api.auth("your_refresh_token").await?;
// Set Accept-Language so tags come back with Chinese translations
// (default is Japanese only)
api.set_accept_lang("zh-CN").await?;
// Search illustrations
// If the access token expires, you'll get a 401. Handle it explicitly:
let results = match api.search_illust("landscape", None).await {
Err(e) if e.is_auth_error() => {
api.refresh_token().await?;
api.search_illust("landscape", None).await?
}
other => other?,
};
// Access typed data (if parse succeeded)
if let Some(data) = &results.data {
println!("Got response: {}", serde_json::to_string(data).unwrap().len());
}
// Always access raw JSON (works even if Pixiv changes the API)
println!("Raw: {}", serde_json::to_string_pretty(&results.raw).unwrap());
Ok(())
}# Authenticate once (saves token to config file)
pixiv-dl auth --token your_token_here
# Search
pixiv-dl search "landscape" --sort popular_desc
# View illustration details
pixiv-dl illust 12345
# Download illustrations
pixiv-dl download 12345 12346 -o ./images/The examples/ directory contains runnable demos:
| Example | Description | Run |
|---|---|---|
get_token |
Obtain a refresh token via OAuth2 PKCE | cargo run -p pixiv-client --example get_token |
basic_usage |
Search illustrations and get details | cargo run -p pixiv-client --example basic_usage |
user_profile |
Fetch your profile and recent illustrations | cargo run -p pixiv-client --example user_profile |
download_illusts |
Search and download illustrations | cargo run -p pixiv-client --example download_illusts |
bookmark_manager |
List, add, and remove bookmarks | cargo run -p pixiv-client --example bookmark_manager |
All examples require the PIXIV_REFRESH_TOKEN environment variable (except get_token).
A command-line tool for searching, viewing, and downloading Pixiv illustrations.
Download pre-built binary from Releases (no Rust toolchain needed):
# Linux / macOS
curl -LO https://github.com/modenicheng/pixiv-api/releases/latest/download/pixiv-dl-x86_64-unknown-linux-gnu.tar.gz
tar xzf pixiv-dl-*.tar.gz && chmod +x pixiv-dl
sudo mv pixiv-dl /usr/local/bin/# Windows (PowerShell)
Invoke-WebRequest https://github.com/modenicheng/pixiv-api/releases/latest/download/pixiv-dl-x86_64-pc-windows-msvc.zip -OutFile pixiv-dl.zip
Expand-Archive pixiv-dl.zip -DestinationPath pixiv-dlThen add the pixiv-dl folder to your PATH so the binary is available everywhere.
Or build from source:
cargo install --path pixiv-dlAuthenticate once and the token is saved automatically:
pixiv-dl auth --token your_token_hereThe token is stored in your platform's config directory:
- Windows:
%APPDATA%\pixiv-dl\config.json - Linux/macOS:
~/.config/pixiv-dl/config.json
After authentication, all commands (search, illust, download) work without extra flags.
Alternatively, set the environment variable (overrides saved config):
# Linux/macOS
export PIXIV_REFRESH_TOKEN=your_token_here
# Windows (PowerShell)
$env:PIXIV_REFRESH_TOKEN = "your_token_here"You can also use the interactive OAuth2 PKCE flow:
pixiv-dl auth --oauthpixiv-dl search <KEYWORD> [OPTIONS]| Option | Description | Default |
|---|---|---|
--sort, -s |
Sort order | date_desc |
--offset, -o |
Page offset | 0 |
Sort options: date_desc, date_asc, popular_desc, popular_male_desc, popular_female_desc
Examples:
# Search for illustrations, newest first
pixiv-dl search "landscape"
# Search sorted by popularity
pixiv-dl search "猫" --sort popular_desc
# Search with offset (pagination)
pixiv-dl search "初音ミク" --sort popular_desc --offset 30
# Search in Japanese
pixiv-dl search "東方Project"pixiv-dl illust <ID>Examples:
# View illustration details as formatted JSON
pixiv-dl illust 12345
# Use with jq for specific fields
pixiv-dl illust 12345 | jq '.illust.title'pixiv-dl download <IDS>... [OPTIONS]| Option | Description | Default |
|---|---|---|
--output, -o |
Output directory | ./images |
--size, -s |
Image resolution: original, large, or medium |
original |
--concurrency, -j |
Max parallel downloads | 4 |
Page selection syntax:
Use id[0,2,3] to download specific pages of a multi-page illustration. Page indices are zero-based. Bare IDs download all pages.
Examples:
# Download a single illustration (all pages)
pixiv-dl download 12345
# Download multiple illustrations in parallel
pixiv-dl download 12345 12346 12347
# Select specific pages from a multi-page illustration
pixiv-dl download 12345[0,2,3]
# Mixed IDs — some with page filter, some full
pixiv-dl download 12345[0] 99999 55555[1,3]
# Download at large resolution with 8 concurrent workers
pixiv-dl download 12345 -s large -j 8
# Download to a custom directory
pixiv-dl download 12345 -o ./my_art# 1. Authenticate (one-time, saves token to config)
pixiv-dl auth --token your_token_here
# 2. Search for something
pixiv-dl search "landscape" --sort popular_desc
# 3. View details of an interesting result
pixiv-dl illust 12345
# 4. Download it
pixiv-dl download 12345 -o ./downloadsThe CLI outputs JSON, so you can combine it with tools like jq:
# Get illustration IDs from search results
pixiv-dl search "landscape" | jq '.illusts[].id'
# Download all illustrations from a search
pixiv-dl search "landscape" | jq -r '.illusts[].id' | xargs -I {} pixiv-dl download {}
# Extract image URLs
pixiv-dl illust 12345 | jq '.illust.image_urls.large'Run the included helper:
cargo run -p pixiv-client --example get_tokenThis will guide you through the OAuth2 PKCE flow:
- Open a URL in your browser
- Log in to Pixiv and authorize
- Copy the redirect URL back to the terminal
- Receive your refresh token
let api = PixivApi::new();
// Authenticate with refresh token
api.auth("your_refresh_token").await?;
// Or set tokens manually
api.set_auth("access_token", "refresh_token", user_id).await;
// Check auth status
assert!(api.is_authenticated().await);
assert_eq!(api.user_id().await, Some(12345));Set per-request headers at runtime. The most common use case is Accept-Language — Pixiv returns tags in Japanese by default; setting this header gives you translated tag names.
// Tags will include Chinese (zh-CN), English (en), etc. translations
api.set_accept_lang("zh-CN").await?;
// Or set any header by name
use reqwest::header::HeaderName;
api.set_header(HeaderName::from_static("x-custom"), "value").await?;
// Remove or clear
api.remove_header(HeaderName::from_static("x-custom")).await;
api.clear_headers().await;| Method | Description |
|---|---|
set_accept_lang(lang) |
Set Accept-Language header (1.1.0) |
set_header(name, value) |
Set an arbitrary header (1.1.0) |
remove_header(name) |
Remove a custom header (1.1.0) |
clear_headers() |
Remove all custom headers (1.1.0) |
custom_headers_snapshot() |
Get current custom headers (1.1.0) |
| Method | Description |
|---|---|
user_detail(user_id) |
Get user details |
user_illusts(user_id, type, offset) |
Get user's illustrations |
user_bookmarks_illust(user_id, restrict, max_id, tag) |
Get bookmarked illustrations |
user_bookmarks_novel(user_id, restrict, max_id) |
Get bookmarked novels |
user_related(user_id) |
Get related users |
user_recommended() |
Get recommended users |
user_following(user_id, restrict, offset) |
Get following list |
user_follower(user_id, offset) |
Get followers |
user_mypixiv(user_id, offset) |
Get Pixiv friends |
user_list(user_ids) |
Get users by IDs |
user_novels(user_id, offset) |
Get user's novels |
user_follow_add(user_id, restrict) |
Follow a user |
user_follow_delete(user_id) |
Unfollow a user |
user_bookmark_tags_illust(user_id, restrict) |
Get bookmark tags |
user_edit_ai_show_settings(ai_type) |
Edit AI show settings |
| Method | Description |
|---|---|
illust_detail(illust_id) |
Get illustration details |
illust_comments(illust_id, offset) |
Get comments |
illust_related(illust_id) |
Get related illustrations |
illust_recommended() |
Get recommended illustrations |
illust_ranking(mode, date, offset) |
Get ranking |
illust_follow(restrict) |
Get followed artists' new works |
illust_new() |
Get newest illustrations |
illust_bookmark_detail(illust_id) |
Get bookmark status |
illust_bookmark_add(illust_id, restrict, tags) |
Add bookmark |
illust_bookmark_delete(illust_id) |
Remove bookmark |
| Method | Description |
|---|---|
novel_detail(novel_id) |
Get novel details |
novel_comments(novel_id, offset) |
Get comments |
novel_recommended() |
Get recommended novels |
novel_new() |
Get newest novels |
novel_follow(restrict) |
Get followed artists' new novels |
novel_series(series_id) |
Get series info |
novel_text(novel_id) |
Get novel text |
webview_novel(novel_id) |
Get novel via webview |
| Method | Description |
|---|---|
search_illust(word, options) |
Search illustrations (pass a SearchOptions) |
search_novel(word, sort, target, offset) |
Search novels |
search_user(word, offset) |
Search users |
trending_tags_illust() |
Get trending tags |
| Method | Description |
|---|---|
ugoira_metadata(illust_id) |
Get UGOIRA animation metadata |
showcase_article(showcase_id) |
Get showcase article |
All API methods return ApiResponse<T> — a hybrid wrapper carrying both typed data and raw JSON:
pub struct ApiResponse<T> {
pub data: Option<T>, // Parsed typed struct (None if parse fails)
pub raw: serde_json::Value, // Raw JSON (always available)
}Important: Pixiv may change their API without notice. Always write a raw JSON fallback route:
let resp = api.search_illust("keyword", None).await?;
// Try typed access first
if let Some(data) = &resp.data {
// Use typed fields
}
// Always have a raw fallback
let raw = &resp.raw;models::illust::Illust— illustration with title, tags, image URLs, etc.models::user::User— user with profile, workspace, etc.models::novel::Novel— novel with text length, series, etc.models::search::{SearchSort, SearchDuration, SearchTarget}— search enumsmodels::common::{Tag, Pagination, ImageUrls, MetaPage}— shared types
All errors are wrapped in PixivError:
use pixiv_client::PixivError;
match api.illust_detail(12345).await {
Ok(resp) => { /* ... */ }
Err(PixivError::Auth(msg)) => eprintln!("Auth error: {msg}"),
Err(PixivError::Status(code)) => eprintln!("HTTP {code}"),
Err(PixivError::Request(e)) => eprintln!("Request failed: {e}"),
Err(PixivError::Parse(e)) => eprintln!("Parse error: {e}"),
Err(e) => eprintln!("Other error: {e}"),
}use pixiv_client::downloader::{DownloadManager, DownloadTask, ProgressEvent, resolve_download_tasks};
// Build download tasks from an illustration (handles single/multi-page)
let tasks = resolve_download_tasks(&illust, "original", Some(&[0, 2])); // pages 0 and 2
// Download with retry and progress
let dm = DownloadManager::new(reqwest::Client::new(), "./images");
let results = dm.download_all(&tasks, 4, |evt| match evt {
ProgressEvent::Started { filename, .. } => println!("Downloading {filename}..."),
ProgressEvent::Finished { filename, path } => println!("Saved {filename} -> {}", path.display()),
ProgressEvent::Failed { filename, error, attempt } => eprintln!("Retry {attempt} for {filename}: {error}"),
_ => {},
}).await;Enable the gfw-bypass feature:
pixiv-client = { version = "1.1", features = ["gfw-bypass"] }Use it to resolve Pixiv's real IP via DNS-over-HTTPS:
#[cfg(feature = "gfw-bypass")]
{
let api = PixivApi::new();
let ip = api.resolve_pixiv_ip().await?;
println!("Pixiv real IP: {ip}");
}use pixiv_client::config::{Config, ClientConfig};
use pixiv_client::PixivApi;
// Custom config
let config = Config {
host: "https://custom.host",
..Default::default()
};
let client_config = ClientConfig {
timeout: Duration::from_secs(60),
proxy: Some("http://127.0.0.1:7890".into()),
..Default::default()
};
let api = PixivApi::with_config(config, client_config);Before (v1.0–v1.1): API calls silently retried on HTTP 401 by refreshing the token internally.
After (v1.2.0): API calls return Err(PixivError::Status(401)) directly. You must refresh explicitly:
match api.search_illust("keyword", None).await {
Err(e) if e.is_auth_error() => {
api.refresh_token().await?; // renew tokens
// retry your call
api.search_illust("keyword", None).await?
}
other => other?,
}The is_auth_error() helper on PixivError makes it easy to match 401s. This change gives you full control over when and how tokens are refreshed.
- Explicit token refresh — removed implicit 401 auto-retry. Call
refresh_token()explicitly when you get a 401.is_auth_error()helper added for ergonomic matching. See Migration Guide. - Custom headers — set per-request headers at runtime via
set_header(),set_accept_lang(), and related methods. Useful forAccept-Languagelocalization and custom headers. - Model fixes — corrected deserialization for
IllustBookmarkDetailResult,TrendingTag,NovelSeriesResult,NovelSeriesInfo, andUserPreviewto match real Pixiv API responses.
This crate is a Rust port of pixivpy by upbit. The API endpoints, authentication flow, and request signing logic are based on pixivpy's implementation.
If you find this crate useful, consider starring the original project as well.
MIT