booru-rs

Crates.iobooru-rs
lib.rsbooru-rs
version0.3.1
created_at2022-11-22 16:08:30.991301+00
updated_at2025-12-24 10:25:06.720444+00
descriptionAn async Booru client for Rust
homepage
repositoryhttps://github.com/ajiiisai/booru-rs
max_upload_size
id720933
size252,392
Ame (ajiiisai)

documentation

https://docs.rs/booru-rs

README

ci-badge crates.io version docs.rs

booru-rs

An async Rust client for various booru image board APIs.

Features

  • Type-safe API — Compile-time checks ensure you use the correct rating types for each booru
  • Async/await — Built on tokio and reqwest for efficient async I/O
  • Connection pooling — Shared HTTP client with automatic connection reuse
  • Proper error handling — No panics, all errors are returned as Result types
  • Common Post trait — Write generic code that works with any booru site
  • Async streams — Paginate through results with async iterators
  • Image downloads — Download images with progress tracking and concurrent downloads
  • Automatic retries — Transient failures are retried with exponential backoff
  • Rate limiting — Protect against API throttling
  • Response caching — Reduce redundant API calls
  • Tag validation — Catch common mistakes before making requests
  • Tag autocomplete — Get tag suggestions as users type

Supported Sites

Site Client Tag Limit Auth Required
Danbooru DanbooruClient 2 No
Gelbooru GelbooruClient Unlimited Yes
Safebooru SafebooruClient Unlimited No
Rule34 Rule34Client Unlimited Yes

Note: Gelbooru and Rule34 require API credentials. See Authentication below.

Planned:

  • Konachan
  • 3DBooru

Installation

Add to your Cargo.toml:

[dependencies]
booru-rs = "0.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

Selective Features

By default, all booru clients are included. You can enable only the ones you need:

[dependencies]
# Only Danbooru support
booru-rs = { version = "0.3", default-features = false, features = ["danbooru"] }

# Danbooru + Safebooru
booru-rs = { version = "0.3", default-features = false, features = ["danbooru", "safebooru"] }

Available features: danbooru, gelbooru, safebooru, rule34

Quick Start

Use the prelude for convenient imports:

use booru_rs::prelude::*;

#[tokio::main]
async fn main() -> Result<()> {
    let posts = GelbooruClient::builder()
        .tag("kafuu_chino")?
        .tag("2girls")?
        .rating(GelbooruRating::General)
        .sort(Sort::Random)  // Or use .random() shorthand
        .limit(5)
        .blacklist_tag(GelbooruRating::Explicit)
        .build()
        .get()
        .await?;

    for post in &posts {
        println!("Post #{}: {}", post.id, post.file_url);
    }

    Ok(())
}

API Examples

Basic Usage

use booru_rs::prelude::*;

// Danbooru (limited to 2 tags)
let posts = DanbooruClient::builder()
    .tag("cat_ears")?
    .rating(DanbooruRating::General)
    .limit(10)
    .build()
    .get()
    .await?;

// Get a specific post by ID
let post = DanbooruClient::builder()
    .build()
    .get_by_id(12345)
    .await?;

Multiple Tags at Once

use booru_rs::prelude::*;

// Gelbooru has no tag limit
let posts = GelbooruClient::builder()
    .tags(["cat_ears", "blue_eyes", "1girl"])?
    .blacklist_tags(["ugly", "low_quality"])
    .sort(Sort::Score)
    .build()
    .get()
    .await?;

Generic Code with the Post Trait

use booru_rs::prelude::*;
use booru_rs::Post;

fn print_post_info(post: &impl Post) {
    println!("#{}: {}x{}", post.id(), post.width(), post.height());
    if let Some(url) = post.file_url() {
        println!("  URL: {}", url);
    }
}

Pagination

use booru_rs::prelude::*;

// Get page 5 of results
let posts = SafebooruClient::builder()
    .tag("landscape")?
    .page(5)
    .limit(100)
    .build()
    .get()
    .await?;

Async Pagination Stream

use booru_rs::prelude::*;

// Stream through all results automatically
let mut stream = SafebooruClient::builder()
    .tag("landscape")?
    .limit(100)
    .into_post_stream()
    .max_posts(500);  // Stop after 500 posts

while let Some(post) = stream.next().await {
    println!("Post #{}", post?.id);
}

Rate Limiting

use booru_rs::prelude::*;
use std::time::Duration;

// 2 requests per second
let limiter = RateLimiter::new(2, Duration::from_secs(1));

for tag in ["cat", "dog", "bird"] {
    limiter.acquire().await;  // Waits if needed
    let posts = SafebooruClient::builder()
        .tag(tag)?
        .build()
        .get()
        .await?;
}

Response Caching

use booru_rs::prelude::*;

let cache = Cache::new();

// First call hits the API
let posts = SafebooruClient::builder()
    .tag("nature")?
    .build()
    .get()
    .await?;
cache.insert("nature_search".to_string(), &posts).await;

// Later, check cache first
if let Some(cached) = cache.get::<Vec<SafebooruPost>>(&"nature_search".to_string()).await {
    println!("Cache hit! {} posts", cached.len());
}

Tag Validation

use booru_rs::validation::{validate_tag, validate_tag_strict};

// Get warnings about potential issues
let result = validate_tag("cat ears");  // Space instead of underscore
if result.has_warnings() {
    println!("Suggested: {}", result.tag());  // "cat_ears"
}

// Or get normalized tag directly
let tag = validate_tag_strict("  cat ears  ")?;  // Returns "cat_ears"

Tag Autocomplete

Get tag suggestions as the user types:

use booru_rs::prelude::*;

// Get tag suggestions for "cat_"
let suggestions = DanbooruClient::autocomplete("cat_", 10).await?;

for tag in suggestions {
    println!("{}: {} posts", tag.name, tag.post_count.unwrap_or(0));
    if let Some(category) = tag.category_name() {
        println!("  Category: {}", category);
    }
}

// Works with all booru clients
let safebooru_tags = SafebooruClient::autocomplete("land", 5).await?;
let gelbooru_tags = GelbooruClient::autocomplete("blue", 5).await?;

Custom HTTP Client

use booru_rs::prelude::*;
use booru_rs::client::ClientBuilder;

let custom_client = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(60))
    .build()?;

// Use with_client to create a builder with custom HTTP client
let posts = ClientBuilder::<SafebooruClient>::with_client(custom_client)
    .tag("nature")?
    .build()
    .get()
    .await?;

Note: Most users won't need a custom HTTP client. The default shared client provides connection pooling and sensible timeouts.

Downloading Images

use booru_rs::prelude::*;
use std::path::Path;

let posts = SafebooruClient::builder()
    .tag("landscape")?
    .limit(10)
    .build()
    .get()
    .await?;

// Download posts directly
let downloader = Downloader::new();
for post in &posts {
    let result = downloader.download_post(post, Path::new("./downloads")).await?;
    println!("Downloaded: {}", result.path.display());
}

// Download with progress tracking
for post in &posts {
    let result = downloader
        .download_post_with_progress(post, Path::new("./downloads"), |progress| {
            println!("{}/{} bytes (post #{})", 
                progress.downloaded, 
                progress.total.unwrap_or(0),
                progress.post_id);
        })
        .await?;
}

// Concurrent downloads (4 at a time)
let results = downloader.download_posts(&posts, Path::new("./downloads"), 4).await;

// Custom options
let downloader = Downloader::new()
    .options(DownloadOptions::default().overwrite().filename("{id}_{md5}.{ext}"));

Gelbooru Authentication

Gelbooru requires API credentials for all API requests. To get your credentials:

  1. Create an account at gelbooru.com
  2. Go to My AccountOptionsAPI Access Credentials
  3. Copy your API Key and User ID
use booru_rs::prelude::*;

let posts = GelbooruClient::builder()
    .set_credentials("your_api_key", "your_user_id")
    .tag("landscape")?
    .build()
    .get()
    .await?;

You can also load credentials from environment variables:

use booru_rs::prelude::*;

let api_key = std::env::var("GELBOORU_API_KEY").expect("GELBOORU_API_KEY not set");
let user_id = std::env::var("GELBOORU_USER_ID").expect("GELBOORU_USER_ID not set");

let posts = GelbooruClient::builder()
    .set_credentials(api_key, user_id)
    .tag("cat_ears")?
    .build()
    .get()
    .await?;

Rule34 Authentication

Rule34 also requires API credentials:

  1. Create an account at rule34.xxx
  2. Go to My AccountOptionsAPI Access Credentials
  3. Copy your API Key and User ID
use booru_rs::prelude::*;

let posts = Rule34Client::builder()
    .set_credentials("your_api_key", "your_user_id")
    .tag("landscape")?
    .build()
    .get()
    .await?;

Error Handling

All fallible operations return Result<T, BooruError>:

use booru_rs::prelude::*;

match DanbooruClient::builder().tag("a")?.tag("b")?.tag("c") {
    Ok(_) => unreachable!(),
    Err(BooruError::TagLimitExceeded { client, max, actual }) => {
        println!("{} only allows {} tags, tried to add {}", client, max, actual);
    }
    Err(e) => println!("Other error: {}", e),
}

Minimum Supported Rust Version

This crate requires Rust 1.92 or later (2024 edition).

License

Licensed under the MIT License.

Commit count: 56

cargo fmt