r34_api/lib.rs
1//! Disclaimer: I made this lil library just for fun, with only a few features,
2//! so I wouldnt recommend using it. But if you use it anyway have fun :>.
3//!
4//!
5//! There may be some parts where you need to know the R34 API, I probably forgot something 🤷♂️. So check out R34's API
6//!
7//!
8//! <https://api.rule34.xxx/>
9//!
10//!
11//! # Lil example
12//! ```
13//! use std::fs;
14//! use r34_api as r34;
15//! use reqwest;
16//!
17//! #[tokio::main]
18//! async fn main() {
19//! // First we make a new Api Url.
20//! // We add the 'big_boobs' tag and a post limit of one so only one
21//! // post will be returned and convert the ApiUrl type into a String.
22//! let request_url = r34::ApiUrl::new().add_tag("big_boobs").set_limit(1).to_api_url();
23//!
24//! // Next we send our request to R34's API.
25//! let api_response = reqwest::get(request_url).await.unwrap();
26//!
27//! // We then parse the json response and get a Vector with Post's.
28//! let posts: Vec<r34::Post> = r34::R34JsonParser::default().from_api_response(api_response).unwrap();
29//!
30//! // Here we get the filename and url of the post's file.
31//! let post_file_url = &posts[0].file_url;
32//! let post_file_name = &posts[0].image;
33//!
34//! // Now we Download the file
35//! let file_as_bytes = reqwest::get(post_file).await.unwrap().bytes().await.unwrap();
36//! // Define its path
37//! let path = format!("./{}", post_file_name);
38//! // And save it.
39//! fs::File::create(path).unwrap().write_all(&file_as_bytes).unwrap();
40//! }
41//! ```
42
43#![allow(dead_code)]
44
45use core::fmt;
46use serde_json::Value;
47use std::{collections::HashMap, fmt::Display, str::FromStr};
48
49/// The ApiUrl Struct is used for easily generating API Urls.
50///
51/// # Example
52/// ```
53/// // It's very simple
54/// // You first need to make a new ApiUrl struct
55/// let api_url_struct = ApiUrl::new();
56///
57/// // Then u can set configurations like tags, the page id or if you wanna go crazy,
58/// // disable the json response and work with html responses.
59/// //!! This Crate has no implementation for html handling so if you want to work with html,
60/// //!! you have to come up with something yourself.
61/// // Lets add only one tag for the start and a request limit of 5.
62///
63/// api_url_struct.add_tag("big_boobs").set_limit(5);
64///
65/// // Now we have to convert our struct into a String that can be used as Url
66///
67/// let api_url = api_url_struct.to_api_url();
68///
69/// // This would be 'https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&tags=big_boobs&limit=5&json=1'
70/// ```
71///
72/// ## With Multiple Tags
73/// ```
74/// let tags: Vec<String> = vec!["big_boobs".to_string(), "big_ass".to_string(), "dark_skin".to_string()];
75/// let api_url = ApiUrl::new().add_tags(tags).set_limit(5).to_api_url();
76///
77/// // And here we have it 'https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&tags=big_boobs big_ass dark_skin&limit=5&json=1'
78/// ```
79
80pub struct ApiUrl {
81 /// Default API Access URL "https://api.rule34.xxx/index.php?page=dapi&s=post&q=index"
82 pub api_url: String,
83 /// Limitter for max Post per APIUrls. R34s API limmits to max 1000 Posts per ApiUrl. Default Setting is 1000.
84 pub req_limit: usize,
85 /// All R34 tags should work when exactly taken over. Default is empty.
86 pub tags: Vec<String>,
87 /// Json Formatted or not Json Formatted: Default is true.
88 pub json: bool,
89 /// Hashmap with the three IDs:
90 /// ID: Filter for ID of Post. (Recommended to use alone)!
91 /// PID: Filter for Page ID.
92 /// CID: Filter for Change ID (Not Recomended)!
93 /// By Default all are empty.
94 pub ids: HashMap<String, Option<usize>>,
95}
96
97impl ApiUrl {
98 /// Creates a new ApiUrl with default settings
99 pub fn new() -> ApiUrl {
100 ApiUrl::default()
101 }
102
103 /// Sets the limit Filter for the ApiUrl
104 pub fn set_limit(mut self, limit: usize) -> Self {
105 self.req_limit = limit;
106 self
107 }
108
109 /// Adds a Tag to the ApiUrl
110 pub fn add_tag(mut self, tag: &'static str) -> Self {
111 self.tags.push(tag.to_string());
112 self
113 }
114
115 pub fn add_tags(mut self, mut tags: Vec<String>) -> Self {
116 self.tags.append(&mut tags);
117 self
118 }
119
120 /// sets the CID of the ApiUrl
121 pub fn set_cid(mut self, cid: usize) -> Self {
122 self.ids.insert("cid".to_string(), Some(cid));
123 self
124 }
125
126 /// Sets the PID of the ApiUrl
127 pub fn set_pid(mut self, pid: usize) -> Self {
128 self.ids.insert("pid".to_string(), Some(pid));
129 self
130 }
131
132 /// Sets the Post ID of the ApiUrl
133 pub fn set_id(mut self, id: usize) -> Self {
134 self.ids.insert("id".to_string(), Some(id));
135 self
136 }
137
138 /// Activates a Json Formatted ApiUrl
139 pub fn set_json_formatted(mut self, json: bool) -> Self {
140 self.json = json;
141 self
142 }
143
144 /// Returns the Final ApiUrl as a String
145 pub fn to_api_url(&mut self) -> String {
146 let api_url = self.api_url.clone();
147 let req_limit = self.req_limit;
148 let json = if self.json == true { r"&json=1" } else { "" };
149 let tags = format!(r"&tags={}", self.tags.join(" "));
150
151 let id = format_id(&self.ids, "id");
152 let pid = format_id(&self.ids, "pid");
153 let cid = format_id(&self.ids, "cid");
154
155 let req_string = format!(
156 r"{}{}&limit={}{}{}{}{}",
157 api_url, tags, req_limit, json, id, pid, cid
158 );
159
160 req_string
161 }
162}
163
164impl Default for ApiUrl {
165 fn default() -> Self {
166 let mut ids: HashMap<String, Option<usize>> = HashMap::new();
167 ids.insert("id".to_string(), None);
168 ids.insert("pid".to_string(), None);
169 ids.insert("cid".to_string(), None);
170
171 ApiUrl {
172 api_url: String::from("https://api.rule34.xxx/index.php?page=dapi&s=post&q=index"),
173 req_limit: 1000,
174 tags: Vec::new(),
175 json: true,
176 ids,
177 }
178 }
179}
180
181fn format_id(ids: &HashMap<String, Option<usize>>, key: &str) -> String {
182 match ids.get(key) {
183 Some(Some(value)) => format!("&{}={}", key, value),
184 _ => String::new(),
185 }
186}
187
188/// A Parser for R34 API Json responses.
189///
190/// Holds a HashMap of config options that can all be tweaked with the `set_conf()` function.
191/// See set_conf() for more information.
192///
193/// # Example
194/// ```
195/// let api_response: &str = ...;
196///
197/// // First we make a new Parser.
198/// let r34_json_parser = r34::R34JsonParser::new();
199///
200/// // Then we take our parser and the api response and parse it.
201/// // That will return a Vector with every Post of the api response.
202/// let posts: Vec<Post> = r34_json_parser.from_api_response(api_response);
203/// ```
204pub struct R34JsonParser {
205 pub conf: HashMap<&'static str, bool>,
206}
207
208impl Default for R34JsonParser {
209 fn default() -> Self {
210 let mut conf: HashMap<&str, bool> = HashMap::new();
211 conf.insert("file_url", true);
212 conf.insert("image", true);
213 conf.insert("tags", true);
214 conf.insert("width", true);
215 conf.insert("height", true);
216 conf.insert("sample", true);
217 conf.insert("samlpe_url", true);
218 conf.insert("sample_width", true);
219 conf.insert("sample_height", true);
220 conf.insert("source", true);
221 conf.insert("id", true);
222 conf.insert("score", true);
223 conf.insert("parent_id", true);
224 conf.insert("comment_count", true);
225 conf.insert("preview_url", true);
226 conf.insert("owner", true);
227 conf.insert("rating", true);
228
229 R34JsonParser { conf }
230 }
231}
232
233impl R34JsonParser {
234 pub fn new() -> R34JsonParser {
235 R34JsonParser::default()
236 }
237
238 /// Takes the response of the r34 api and returns a Vector of the parsed Posts
239 /// # Errors
240 /// This function will return and Error if the response is empty.
241 /// An empty response is most likley caused by wrong configurations like wrong Tags.
242 pub fn from_api_response(&mut self, s: &str) -> Result<Vec<Post>, R34Error> {
243 if s == "" {
244 return Err(R34Error::R34EmptyReturn(String::from("One or more Tags didn't exist.")));
245 }
246
247 let value = match serde_json::Value::from_str(&s) {
248 Ok(value) => value,
249 Err(e) => {
250 return Err(R34Error::JsonParseError(e));
251 }
252 };
253
254 let pretty_json = serde_json::to_string_pretty(&value).unwrap();
255
256 Ok(self.parse_json(&pretty_json).unwrap())
257 }
258
259 /// Takes a valid json string and returns a Vector of Posts with all information configured.
260 pub fn parse_json(&mut self, s: &str) -> Result<Vec<Post>, R34Error> {
261 if s == "" {
262 return Err(R34Error::R34EmptyReturn(String::from("One or more Tags didn't exist.")));
263 }
264
265 let value: Value = match serde_json::Value::from_str(&s) {
266 Ok(value) => value,
267 Err(e) => {
268 return Err(R34Error::JsonParseError(e));
269 }
270 };
271
272 let mut post_vec: Vec<Post> = Vec::new();
273
274 if let Value::Array(a) = value {
275 for obj in a {
276 let mut post = Post::default();
277
278 for (k, b) in &mut self.conf {
279 match (k, b) {
280
281 (&"file_url", true) => {
282 if let Value::Object(ref map) = obj {
283 if let Some(Value::String(s)) = map.get("file_url") {
284 post.file_url = s.clone();
285 }
286 }
287 }
288
289 (&"image", true) => {
290 if let Value::Object(ref map) = obj {
291 if let Some(Value::String(s)) = map.get("image") {
292 post.image = s.clone();
293 }
294 }
295 }
296 (&"tags", true) => {
297 if let Value::Object(ref map) = obj {
298 if let Some(Value::String(s)) = map.get("tags") {
299 post.tags = s
300 .clone()
301 .split_whitespace()
302 .map(move |s| s.to_string())
303 .collect();
304 }
305 }
306 }
307 (&"width", true) => {
308 if let Value::Object(ref map) = obj {
309 if let Some(Value::Number(n)) = map.get("width") {
310 post.width = n.as_u64().unwrap();
311 }
312 }
313 }
314 (&"height", true) => {
315 if let Value::Object(ref map) = obj {
316 if let Some(Value::Number(n)) = map.get("height") {
317 post.height = n.as_u64().unwrap();
318 }
319 }
320 }
321 (&"sample", true) => {
322 if let Value::Object(ref map) = obj {
323 if let Some(Value::Bool(b)) = map.get("sample") {
324 post.sample = b.clone();
325 }
326 }
327 }
328 (&"sample_url", true) => {
329 if let Value::Object(ref map) = obj {
330 if let Some(Value::String(s)) = map.get("sample_url") {
331 post.sample_url = s.clone();
332 }
333 }
334 }
335 (&"sample_width", true) => {
336 if let Value::Object(ref map) = obj {
337 if let Some(Value::Number(n)) = map.get("sample_width") {
338 post.sample_width = n.as_u64().unwrap();
339 }
340 }
341 }
342 (&"sample_height", true) => {
343 if let Value::Object(ref map) = obj {
344 if let Some(Value::Number(n)) = map.get("sample_height") {
345 post.sample_height = n.as_u64().unwrap();
346 }
347 }
348 }
349 (&"source", true) => {
350 if let Value::Object(ref map) = obj {
351 if let Some(Value::String(s)) = map.get("source") {
352 post.source = s.clone();
353 }
354 }
355 }
356 (&"id", true) => {
357 if let Value::Object(ref map) = obj {
358 if let Some(Value::Number(n)) = map.get("id") {
359 post.id = n.as_u64().unwrap();
360 }
361 }
362 }
363 (&"score", true) => {
364 if let Value::Object(ref map) = obj {
365 if let Some(Value::Number(n)) = map.get("score") {
366 post.score = n.as_u64().unwrap();
367 }
368 }
369 }
370 (&"parent_id", true) => {
371 if let Value::Object(ref map) = obj {
372 if let Some(Value::Number(n)) = map.get("parent_id") {
373 post.parent_id = n.as_u64().unwrap();
374 }
375 }
376 }
377 (&"comment_count", true) => {
378 if let Value::Object(ref map) = obj {
379 if let Some(Value::Number(n)) = map.get("comment_count") {
380 post.comment_count = n.as_u64().unwrap();
381 }
382 }
383 }
384 (&"preview_url", true) => {
385 if let Value::Object(ref map) = obj {
386 if let Some(Value::String(s)) = map.get("preview_url") {
387 post.preview_url = s.clone();
388 }
389 }
390 }
391 (&"owner", true) => {
392 if let Value::Object(ref map) = obj {
393 if let Some(Value::String(s)) = map.get("owner") {
394 post.owner = s.clone();
395 }
396 }
397 }
398 (&"rating", true) => {
399 if let Value::Object(ref map) = obj {
400 if let Some(Value::String(s)) = map.get("rating") {
401 match s.as_str() {
402 "explicit" => post.rating = Some(Rating::Explicit),
403 "safe" => post.rating = Some(Rating::Safe),
404 "questionable" => post.rating = Some(Rating::Questionable),
405 _ => post.rating = None,
406 }
407 }
408 }
409 }
410 _ => (),
411 }
412 }
413 post_vec.push(post);
414 }
415 }
416 Ok(post_vec)
417 }
418
419 /// Set Conifg options with the name of the field and a bool.
420 ///
421 /// Will be changed to enum or something
422 /// All keys/fieldnames are &str's e.g. `"file_url"` `"id"`
423 ///
424 ///
425 /// Possible Keys:
426 /// ```
427 /// "id";
428 /// "parent_id";
429 /// "image";
430 /// "tags";
431 /// "source";
432 /// "owner";
433 /// "score";
434 /// "comment_count";
435 /// "rating";
436 /// "sample";
437 /// "file_url";
438 /// "sample_url";
439 /// "preview_url";
440 /// "width";
441 /// "height";
442 /// "sample_width";
443 /// "sample_height";
444 /// ```
445 pub fn set_conf(mut self, key: &'static str, set: bool) -> Self {
446 *self.conf.get_mut(&key).unwrap() = set;
447 self
448 }
449}
450
451/// Specifically used for Parsing of Json API Rseponses
452#[derive(Debug)]
453pub enum R34Error {
454 /// Wrapper so i can use my own Error (Will be improved in future)
455 JsonParseError(serde_json::Error),
456 /// Returns when a Json file is empty
457 R34EmptyReturn(String),
458}
459
460impl std::fmt::Display for R34Error {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 match self {
463 R34Error::JsonParseError(e) => write!(f, "{}", e),
464 R34Error::R34EmptyReturn(e) => write!(f, "{}", e)
465 }
466 }
467}
468
469/// Rating of the Post as enum.
470/// Will probably get removed in the future!
471#[derive(Clone, Copy)]
472pub enum Rating {
473 Explicit,
474 Safe,
475 Questionable,
476}
477
478impl fmt::Display for Rating {
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 let r = match *self {
481 Self::Explicit => "explicit",
482 Self::Safe => "safe",
483 Self::Questionable => "questionable",
484 };
485 write!(f, "{}", r)
486 }
487}
488
489/// Post struct. Holds all information about a Post
490#[derive(Clone)]
491pub struct Post {
492 /// Url of Post's File
493 pub file_url: String,
494 /// Name of Post's File
495 pub image: String,
496 /// Post's Tags
497 pub tags: Vec<String>,
498 /// Width of Post's File
499 pub width: u64,
500 /// Height of Post's File
501 pub height: u64,
502
503 /// Tells if the Post has a Sample
504 pub sample: bool,
505 /// Url of Post's Sample
506 pub sample_url: String,
507
508 /// Height of Post's Sample File
509 pub sample_width: u64,
510 /// Height of Post's Sample File
511 pub sample_height: u64,
512 /// Source of Post e.g. Twitter url etc.
513 pub source: String,
514 /// ID of Post
515 pub id: u64,
516 /// Score of Post
517 pub score: u64,
518 /// ID of Post's Parent Post
519 pub parent_id: u64,
520 /// Amount of Comments on the Post
521 pub comment_count: u64,
522 /// Url of Post's Preview Image
523 pub preview_url: String,
524 /// Name of Post's Owner/Poster
525 pub owner: String,
526 /// Rating of Post e.g. Safe, Explicit or Questionable
527 pub rating: Option<Rating>,
528}
529
530impl Display for Post {
531 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532 write!(f,
533 "image: {}\nfile_url: {}\nwidth: {}\nheight: {}\ntags: {:?}\nid: {}\nowner: {}\nrating: {}\nsample_url: {}\nsample_width: {}\nsample_height: {}\nsource: {}\nscore: {}\nparent_id: {}\ncomment_count: {}\npreview_url: {}\n",
534 self.image, self.file_url, self.width,
535 self.height, self.tags, self.id,
536 self.owner, self.rating.clone().unwrap(), self.sample_url,
537 self.sample_width, self.sample_height,
538 self.source, self.score, self.parent_id,
539 self.comment_count, self.preview_url)
540 }
541}
542
543impl Default for Post {
544 fn default() -> Self {
545 Post {
546 file_url: String::new(),
547 width: 0,
548 height: 0,
549 image: String::new(),
550 tags: vec![],
551 sample: false,
552 sample_url: String::new(),
553 sample_width: 0,
554 sample_height: 0,
555 source: String::new(),
556 id: 1,
557 score: 0,
558 parent_id: 0,
559 comment_count: 0,
560 preview_url: String::new(),
561 owner: String::new(),
562 rating: None,
563 }
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use std::{
570 fs,
571 io::{Read, Write},
572 time,
573 };
574
575 #[test]
576 fn bench_json_parse_this() {
577 let mut buf = String::from("");
578 fs::File::open("./response.json")
579 .unwrap()
580 .read_to_string(&mut buf)
581 .unwrap();
582 let json = buf.as_str();
583
584 let now = time::Instant::now();
585 let _posts = super::R34JsonParser::default().parse_json(json);
586 let elapsed = now.elapsed();
587 println!("Full: {:?}", elapsed);
588
589 // let _file = fs::OpenOptions::new()
590 // .write(true)
591 // .append(true)
592 // .open("./test.txt")
593 // .unwrap()
594 // .write(format!("Full: {}\n", elapsed.as_nanos()).as_bytes())
595 // .unwrap();
596 }
597
598 #[test]
599 fn test_json_parse_this() {
600 let mut buf = String::from("");
601 fs::File::open("./response.json")
602 .unwrap()
603 .read_to_string(&mut buf)
604 .unwrap();
605 let json = buf.as_str();
606
607 let posts = super::R34JsonParser::default().parse_json(json).unwrap();
608 let post1 = &posts[0].to_string();
609 let post2 = &posts[1].to_string();
610
611 let _file = fs::OpenOptions::new()
612 .write(true)
613 .append(true)
614 .open("./test.txt")
615 .unwrap()
616 .write_all(format!("{}\n{}\n", post1, post2).as_bytes()).unwrap();
617 }
618}