booru_rs/client/
rule34.rs1use super::{Client, ClientBuilder, shared_client};
4use crate::autocomplete::{Autocomplete, TagSuggestion};
5use crate::error::{BooruError, Result};
6use crate::model::rule34::*;
7use serde::Deserialize;
8
9#[derive(Debug)]
49pub struct Rule34Client(ClientBuilder<Self>);
50
51impl From<ClientBuilder<Self>> for Rule34Client {
52 fn from(value: ClientBuilder<Self>) -> Self {
53 Self(value)
54 }
55}
56
57impl Client for Rule34Client {
58 type Post = Rule34Post;
59 type Rating = Rule34Rating;
60
61 const URL: &'static str = "https://api.rule34.xxx";
62 const SORT: &'static str = "sort:";
63 const MAX_TAGS: Option<usize> = None;
64
65 async fn get_by_id(&self, id: u32) -> Result<Self::Post> {
73 let builder = &self.0;
74 let url = &builder.url;
75
76 let mut query = vec![
77 ("page", "dapi".to_string()),
78 ("s", "post".to_string()),
79 ("q", "index".to_string()),
80 ("id", id.to_string()),
81 ("json", "1".to_string()),
82 ];
83
84 if let (Some(key), Some(user)) = (&builder.key, &builder.user) {
86 query.push(("api_key", key.clone()));
87 query.push(("user_id", user.clone()));
88 }
89
90 let response = builder
91 .client
92 .get(format!("{url}/index.php"))
93 .query(&query)
94 .send()
95 .await?;
96
97 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
99 return Err(BooruError::Unauthorized(
100 "Rule34 requires API credentials. Use set_credentials(api_key, user_id)".into(),
101 ));
102 }
103
104 let text = response.text().await?;
107 if text.contains("Missing authentication") {
108 return Err(BooruError::Unauthorized(
109 "Rule34 requires API credentials. Use set_credentials(api_key, user_id)".into(),
110 ));
111 }
112
113 let posts: Vec<Rule34Post> = serde_json::from_str(&text)?;
114 posts.into_iter().next().ok_or(BooruError::PostNotFound(id))
115 }
116
117 async fn get(&self) -> Result<Vec<Self::Post>> {
124 let builder = &self.0;
125 let url = &builder.url;
126 let tag_string = builder.tags.join(" ");
127
128 let mut query = vec![
129 ("page", "dapi".to_string()),
130 ("s", "post".to_string()),
131 ("q", "index".to_string()),
132 ("pid", builder.page.to_string()),
133 ("limit", builder.limit.to_string()),
134 ("tags", tag_string),
135 ("json", "1".to_string()),
136 ];
137
138 if let (Some(key), Some(user)) = (&builder.key, &builder.user) {
140 query.push(("api_key", key.clone()));
141 query.push(("user_id", user.clone()));
142 }
143
144 let response = builder
145 .client
146 .get(format!("{url}/index.php"))
147 .query(&query)
148 .send()
149 .await?;
150
151 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
153 return Err(BooruError::Unauthorized(
154 "Rule34 requires API credentials. Use set_credentials(api_key, user_id)".into(),
155 ));
156 }
157
158 let text = response.text().await?;
161 if text.contains("Missing authentication") {
162 return Err(BooruError::Unauthorized(
163 "Rule34 requires API credentials. Use set_credentials(api_key, user_id)".into(),
164 ));
165 }
166
167 if text.is_empty() || text == "[]" {
169 return Ok(Vec::new());
170 }
171
172 let posts: Vec<Rule34Post> = serde_json::from_str(&text)?;
173 Ok(posts)
174 }
175}
176
177#[derive(Debug, Deserialize)]
179struct Rule34AutocompleteItem {
180 value: String,
182 label: String,
184}
185
186impl Autocomplete for Rule34Client {
187 async fn autocomplete(query: &str, _limit: u32) -> Result<Vec<TagSuggestion>> {
188 let client = shared_client();
189 let url = "https://api.rule34.xxx/autocomplete.php";
191
192 let response = client.get(url).query(&[("q", query)]).send().await?;
193
194 if response.status() == reqwest::StatusCode::UNAUTHORIZED {
195 return Err(BooruError::Unauthorized(
196 "Rule34 autocomplete request failed".into(),
197 ));
198 }
199
200 let items: Vec<Rule34AutocompleteItem> = response.json().await?;
201
202 Ok(items
203 .into_iter()
204 .map(|item| TagSuggestion {
205 name: item.value,
206 label: item.label.clone(),
207 post_count: parse_post_count_from_label(&item.label),
208 category: None,
209 })
210 .collect())
211 }
212}
213
214fn parse_post_count_from_label(label: &str) -> Option<u32> {
216 let start = label.rfind('(')?;
217 let end = label.rfind(')')?;
218 if start < end {
219 label[start + 1..end].parse().ok()
220 } else {
221 None
222 }
223}