booru_rs/client/
danbooru.rs

1//! Danbooru API client implementation.
2
3use super::{Client, ClientBuilder, shared_client};
4use crate::autocomplete::{Autocomplete, TagSuggestion};
5use crate::error::Result;
6use crate::model::danbooru::*;
7
8use reqwest::header::{self, HeaderMap, HeaderValue};
9use serde::Deserialize;
10
11/// Returns headers required for Danbooru API requests.
12///
13/// Danbooru requires a User-Agent header for requests.
14fn get_headers() -> HeaderMap {
15    let mut headers = HeaderMap::with_capacity(1);
16    headers.insert(
17        header::USER_AGENT,
18        HeaderValue::from_static("booru-rs/0.3.0"),
19    );
20    headers
21}
22
23/// Client for interacting with the Danbooru API.
24///
25/// Danbooru has a limit of 2 tags per query for non-authenticated users.
26///
27/// # Example
28///
29/// ```no_run
30/// use booru_rs::danbooru::{DanbooruClient, DanbooruRating};
31/// use booru_rs::client::Client;
32///
33/// # async fn example() -> booru_rs::error::Result<()> {
34/// let posts = DanbooruClient::builder()
35///     .tag("cat_ears")?
36///     .rating(DanbooruRating::General)
37///     .limit(10)
38///     .build()
39///     .get()
40///     .await?;
41///
42/// println!("Found {} posts", posts.len());
43/// # Ok(())
44/// # }
45/// ```
46#[derive(Debug)]
47pub struct DanbooruClient(ClientBuilder<Self>);
48
49impl From<ClientBuilder<Self>> for DanbooruClient {
50    fn from(value: ClientBuilder<Self>) -> Self {
51        Self(value)
52    }
53}
54
55impl Client for DanbooruClient {
56    type Post = DanbooruPost;
57    type Rating = DanbooruRating;
58
59    const URL: &'static str = "https://danbooru.donmai.us";
60    const SORT: &'static str = "order:";
61    const MAX_TAGS: Option<usize> = Some(2);
62
63    /// Retrieves a single post by its unique ID.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the request fails or if the response cannot be parsed.
68    async fn get_by_id(&self, id: u32) -> Result<Self::Post> {
69        let builder = &self.0;
70        let url = &builder.url;
71
72        let response = builder
73            .client
74            .get(format!("{url}/posts/{id}.json"))
75            .headers(get_headers())
76            .send()
77            .await?
78            .json::<DanbooruPost>()
79            .await?;
80
81        Ok(response)
82    }
83
84    /// Retrieves posts matching the configured query.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the request fails or if the response cannot be parsed.
89    async fn get(&self) -> Result<Vec<Self::Post>> {
90        let builder = &self.0;
91        let tag_string = builder.tags.join(" ");
92        let url = &builder.url;
93
94        let response = builder
95            .client
96            .get(format!("{url}/posts.json"))
97            .headers(get_headers())
98            .query(&[
99                ("limit", builder.limit.to_string()),
100                ("page", builder.page.to_string()),
101                ("tags", tag_string),
102            ])
103            .send()
104            .await?
105            .json::<Vec<DanbooruPost>>()
106            .await?;
107
108        Ok(response)
109    }
110}
111
112/// Danbooru autocomplete API response item.
113#[derive(Debug, Deserialize)]
114struct DanbooruAutocompleteItem {
115    value: String,
116    label: String,
117    category: Option<u8>,
118    post_count: Option<u32>,
119}
120
121impl Autocomplete for DanbooruClient {
122    /// Returns tag suggestions from Danbooru's autocomplete API.
123    ///
124    /// # Example
125    ///
126    /// ```no_run
127    /// use booru_rs::danbooru::DanbooruClient;
128    /// use booru_rs::autocomplete::Autocomplete;
129    ///
130    /// # async fn example() -> booru_rs::error::Result<()> {
131    /// let suggestions = DanbooruClient::autocomplete("cat_", 10).await?;
132    /// for tag in suggestions {
133    ///     println!("{}: {} posts", tag.name, tag.post_count.unwrap_or(0));
134    /// }
135    /// # Ok(())
136    /// # }
137    /// ```
138    async fn autocomplete(query: &str, limit: u32) -> Result<Vec<TagSuggestion>> {
139        let response = shared_client()
140            .get(format!("{}/autocomplete.json", Self::URL))
141            .headers(get_headers())
142            .query(&[
143                ("search[query]", query),
144                ("search[type]", "tag_query"),
145                ("limit", &limit.to_string()),
146            ])
147            .send()
148            .await?
149            .json::<Vec<DanbooruAutocompleteItem>>()
150            .await?;
151
152        Ok(response
153            .into_iter()
154            .map(|item| TagSuggestion {
155                name: item.value,
156                label: item.label,
157                post_count: item.post_count,
158                category: item.category,
159            })
160            .collect())
161    }
162}