booru_rs/client/
gelbooru.rs1use super::{Client, ClientBuilder, shared_client};
4use crate::autocomplete::{Autocomplete, TagSuggestion};
5use crate::error::{BooruError, Result};
6use crate::model::gelbooru::*;
7use serde::Deserialize;
8
9#[derive(Debug)]
44pub struct GelbooruClient(ClientBuilder<Self>);
45
46impl From<ClientBuilder<Self>> for GelbooruClient {
47 fn from(value: ClientBuilder<Self>) -> Self {
48 Self(value)
49 }
50}
51
52impl Client for GelbooruClient {
53 type Post = GelbooruPost;
54 type Rating = GelbooruRating;
55
56 const URL: &'static str = "https://gelbooru.com";
57 const SORT: &'static str = "sort:";
58 const MAX_TAGS: Option<usize> = None;
59
60 async fn get_by_id(&self, id: u32) -> Result<Self::Post> {
68 let builder = &self.0;
69 let url = &builder.url;
70
71 let mut query = vec![
72 ("page", "dapi".to_string()),
73 ("s", "post".to_string()),
74 ("q", "index".to_string()),
75 ("id", id.to_string()),
76 ("json", "1".to_string()),
77 ];
78
79 if let (Some(key), Some(user)) = (&builder.key, &builder.user) {
81 query.push(("api_key", key.clone()));
82 query.push(("user_id", user.clone()));
83 }
84
85 let response = builder
86 .client
87 .get(format!("{url}/index.php"))
88 .query(&query)
89 .send()
90 .await?;
91
92 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
94 return Err(BooruError::Unauthorized(
95 "Gelbooru requires API credentials. Use set_credentials(api_key, user_id)".into(),
96 ));
97 }
98
99 let data = response.json::<GelbooruResponse>().await?;
100
101 data.posts
102 .into_iter()
103 .next()
104 .ok_or(BooruError::PostNotFound(id))
105 }
106
107 async fn get(&self) -> Result<Vec<Self::Post>> {
114 let builder = &self.0;
115 let url = &builder.url;
116 let tag_string = builder.tags.join(" ");
117
118 let mut query = vec![
119 ("page", "dapi".to_string()),
120 ("s", "post".to_string()),
121 ("q", "index".to_string()),
122 ("pid", builder.page.to_string()),
123 ("limit", builder.limit.to_string()),
124 ("tags", tag_string),
125 ("json", "1".to_string()),
126 ];
127
128 if let (Some(key), Some(user)) = (&builder.key, &builder.user) {
130 query.push(("api_key", key.clone()));
131 query.push(("user_id", user.clone()));
132 }
133
134 let response = builder
135 .client
136 .get(format!("{url}/index.php"))
137 .query(&query)
138 .send()
139 .await?;
140
141 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
143 return Err(BooruError::Unauthorized(
144 "Gelbooru requires API credentials. Use set_credentials(api_key, user_id)".into(),
145 ));
146 }
147
148 let data = response.json::<GelbooruResponse>().await?;
149
150 Ok(data.posts)
151 }
152}
153
154#[derive(Debug, Deserialize)]
156struct GelbooruAutocompleteItem {
157 value: String,
159 label: String,
161 #[serde(default)]
163 category: Option<String>,
164 #[serde(default)]
166 post_count: Option<u32>,
167}
168
169impl Autocomplete for GelbooruClient {
170 async fn autocomplete(query: &str, limit: u32) -> Result<Vec<TagSuggestion>> {
171 let client = shared_client();
172 let url = format!("{}/index.php", Self::URL);
173
174 let response = client
175 .get(&url)
176 .query(&[
177 ("page", "autocomplete2"),
178 ("term", query),
179 ("type", "tag_query"),
180 ("limit", &limit.to_string()),
181 ])
182 .send()
183 .await?;
184
185 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
186 return Err(BooruError::Unauthorized(
187 "Gelbooru requires API credentials for some endpoints".into(),
188 ));
189 }
190
191 let items: Vec<GelbooruAutocompleteItem> = response.json().await?;
192
193 Ok(items
194 .into_iter()
195 .map(|item| {
196 let post_count = item
198 .post_count
199 .or_else(|| parse_post_count_from_label(&item.label));
200
201 let category = item.category.as_deref().and_then(parse_category);
203
204 TagSuggestion {
205 name: item.value,
206 label: item.label,
207 post_count,
208 category,
209 }
210 })
211 .collect())
212 }
213}
214
215fn parse_category(cat: &str) -> Option<u8> {
217 match cat.to_lowercase().as_str() {
218 "general" | "tag" => Some(0),
219 "artist" => Some(1),
220 "copyright" | "series" => Some(3),
221 "character" => Some(4),
222 "meta" | "metadata" => Some(5),
223 _ => cat.parse().ok(),
224 }
225}
226
227fn parse_post_count_from_label(label: &str) -> Option<u32> {
229 let start = label.rfind('(')?;
230 let end = label.rfind(')')?;
231 if start < end {
232 label[start + 1..end].parse().ok()
233 } else {
234 None
235 }
236}