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}