gelbooru_api/
api.rs

1//! API types and methods
2//!
3//! Use the associated functions at the root module and `RequestBuilder`s to send requests.
4
5use crate::{Client, Error};
6use hyper::body::Buf;
7use serde::Deserialize;
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::convert::{AsRef, Into};
11
12// marker trait for API types
13trait ApiQuery: serde::de::DeserializeOwned {}
14
15const API_BASE: &'static str = "https://gelbooru.com/index.php?page=dapi&q=index&json=1";
16
17type QueryStrings<'a> = HashMap<&'a str, String>;
18
19#[derive(Deserialize, Debug)]
20pub struct Attributes {
21    pub limit: usize,
22    pub offset: usize,
23    pub count: usize,
24}
25
26#[derive(Deserialize, Debug)]
27pub struct PostQuery {
28    #[serde(rename = "@attributes")]
29    pub attributes: Attributes,
30    #[serde(rename = "post", default = "Vec::new")]
31    pub posts: Vec<Post>,
32}
33
34#[derive(Deserialize, Debug)]
35pub struct TagQuery {
36    #[serde(rename = "@attributes")]
37    pub attributes: Attributes,
38    #[serde(rename = "tag", default = "Vec::new")]
39    pub tags: Vec<Tag>,
40}
41
42/// Post on Gelbooru
43#[derive(Deserialize, Debug)]
44pub struct Post {
45    pub source: String,
46    pub directory: String,
47    pub height: u64,
48    pub id: u64,
49    pub image: String,
50    pub change: u64,
51    pub owner: String,
52    pub parent_id: Option<u64>,
53    pub rating: String,
54    pub sample: u64,
55    pub preview_height: u64,
56    pub preview_width: u64,
57    pub sample_height: u64,
58    pub sample_width: u64,
59    pub score: u64,
60    pub tags: String,
61    pub title: String,
62    pub width: u64,
63    pub file_url: String,
64    pub created_at: String,
65    pub post_locked: u64,
66}
67
68impl ApiQuery for PostQuery {}
69
70impl Post {
71    pub fn id(&self) -> u64 {
72        self.id
73    }
74
75    pub fn title<'a>(&'a self) -> &'a str {
76        &self.title
77    }
78
79    pub fn score(&self) -> u64 {
80        self.score
81    }
82
83    pub fn created_at(&self) -> chrono::DateTime<chrono::offset::FixedOffset> {
84        chrono::DateTime::parse_from_str(&self.created_at, "%a %b %d %H:%M:%S %z %Y")
85            .expect("failed to parse DateTime")
86    }
87
88    pub fn rating<'a>(&'a self) -> Rating {
89        use crate::Rating::*;
90        match &self.rating[0..1] {
91            "s" => Safe,
92            "q" => Questionable,
93            "e" => Explicit,
94            _ => unreachable!("non-standard rating"),
95        }
96    }
97
98    pub fn owner<'a>(&'a self) -> &'a str {
99        &self.owner
100    }
101
102    pub fn tags<'a>(&'a self) -> Vec<&'a str> {
103        self.tags.split(' ').collect()
104    }
105
106    pub fn dimensions(&self) -> (u64, u64) {
107        (self.width, self.height)
108    }
109
110    pub fn image_url<'a>(&'a self) -> &'a str {
111        &self.file_url
112    }
113
114    pub fn source<'a>(&'a self) -> &'a str {
115        &self.source
116    }
117}
118
119/// The content rating of a post.
120///
121/// See [this forum post](https://gelbooru.com/index.php?page=wiki&s=view&id=2535) for an in-depth explanation of the 3 ratings.
122#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
123pub enum Rating {
124    Safe,
125    Questionable,
126    Explicit,
127}
128
129/// Request builder for the Posts endpoint.
130///
131/// See the [`posts`](fn.posts.html) function for proper usage.
132#[derive(Clone, Debug)]
133pub struct PostsRequestBuilder<'a> {
134    pub(crate) limit: Option<usize>,
135    pub(crate) tags: Vec<Cow<'a, str>>,
136    pub(crate) tags_raw: String,
137    pub(crate) rating: Option<Rating>,
138    pub(crate) sort_random: bool,
139}
140
141impl<'a> PostsRequestBuilder<'a> {
142    /// Amount of posts to recieve.
143    ///
144    /// When unspecified, default limit is 100, as set by the server.
145    pub fn limit(mut self, limit: usize) -> Self {
146        self.limit = Some(limit);
147        self
148    }
149
150    /// Add a single tag to the list of tags to search for.
151    /// To clear already set tags, see [`clear_tags`](#method.clear_tags).
152    ///
153    /// ## Example
154    /// ```rust
155    /// # use gelbooru_api::{Client, Error, posts};
156    /// # async fn example() -> Result<(), Error> {
157    /// # let client = Client::public();
158    /// posts()
159    ///     .tag("hello")
160    ///     .tag("world".to_string())
161    ///     .send(&client)
162    ///     .await?;
163    ///
164    /// # Ok(())
165    /// # }
166    /// ```
167    pub fn tag<S: Into<Cow<'a, str>>>(mut self, tag: S) -> Self {
168        self.tags.push(tag.into());
169        self
170    }
171
172    /// Tags to search for.
173    /// Any tag combination that works on the website will work here, including meta-tags.
174    ///
175    /// Can be chained; previously added tags are not overridden.
176    /// To clear already set tags, see [`clear_tags`](#method.clear_tags).
177    ///
178    /// ## Example
179    /// ```rust
180    /// # use gelbooru_api::{Client, Error, posts};
181    /// # async fn example() -> Result<(), Error> {
182    /// # let client = Client::public();
183    /// posts()
184    ///     .tags(&["hello", "world"])
185    ///     .tags(&vec!["how", "are", "you?"])
186    ///     .send(&client)
187    ///     .await?;
188    ///
189    /// # Ok(())
190    /// # }
191    /// ```
192    pub fn tags<S: AsRef<str>>(mut self, tags: &'a [S]) -> Self {
193        let mut other = tags
194            .iter()
195            .map(|s| Cow::from(s.as_ref()))
196            .collect::<Vec<_>>();
197        self.tags.append(&mut other);
198        self
199    }
200
201    /// Append string directly to tag search
202    ///
203    /// !! These are not checked when being submitted !!
204    /// You can easily mess up the query construction using these.
205    ///
206    /// Probably only useful for setting meta-tags.
207    pub fn tags_raw<S: std::string::ToString>(mut self, raw_tags: S) -> Self {
208        self.tags_raw = raw_tags.to_string();
209        self
210    }
211
212    /// Clears the list of tags to search for.
213    /// Tags set using [`tags_raw`](#method.tags_raw) are also cleared.
214    ///
215    /// ## Example
216    /// ```rust
217    /// # use gelbooru_api::{Client, Error, posts};
218    /// # async fn example() -> Result<(), Error> {
219    /// # let client = Client::public();
220    /// posts()
221    ///     .tags(&["herro", "world"])
222    ///     .clear_tags() // wait, nevermind! clear tags.
223    ///     .tags(&["hello", "world"])
224    ///     .send(&client)
225    ///     .await?;
226    /// # Ok(())
227    /// # }
228    /// ```
229    pub fn clear_tags(mut self) -> Self {
230        self.tags = Vec::new();
231        self.tags_raw = String::new();
232        self
233    }
234
235    /// Filter by content ratings.
236    /// See [`Rating`](enum.rating.html).
237    ///
238    /// ## Example
239    /// ```rust
240    /// # use gelbooru_api::{Client, Error, Rating, posts};
241    /// # async fn example() -> Result<(), Error> {
242    /// # let client = Client::public();
243    /// posts()
244    ///     .tags(&["hatsune_miku"])
245    ///     .rating(Rating::Safe)
246    ///     .send(&client)
247    ///     .await?;
248    ///
249    /// # Ok(())
250    /// # }
251    /// ```
252    pub fn rating(mut self, rating: Rating) -> Self {
253        self.rating = Some(rating);
254        self
255    }
256
257    /// Randomize the order of posts.
258    ///
259    /// This is a server-side meta-tag feature, and is only provided for completeness' sake.
260    ///
261    /// ## Example
262    /// ```rust
263    /// # use gelbooru_api::{Client, Error, Rating, posts};
264    /// # async fn example() -> Result<(), Error> {
265    /// # let client = Client::public();
266    /// posts()
267    ///     .tags(&["hatsune_miku"])
268    ///     .random(true)
269    ///     .send(&client)
270    ///     .await?;
271    ///
272    /// # Ok(())
273    /// # }
274    /// ```
275    pub fn random(mut self, random: bool) -> Self {
276        self.sort_random = random;
277        self
278    }
279
280    pub async fn send(self, client: &Client) -> Result<PostQuery, Error> {
281        let mut tags = String::new();
282        if let Some(rating) = self.rating {
283            tags.push_str(&format!("rating:{:?}+", rating).to_lowercase());
284        }
285        if self.sort_random {
286            tags.push_str("sort:random+");
287        }
288        tags.push_str(&self.tags.join("+"));
289        if !self.tags_raw.is_empty() {
290            tags.push('+');
291            tags.push_str(&self.tags_raw);
292        }
293
294        let mut qs: QueryStrings = Default::default();
295        qs.insert("s", "post".to_string());
296        qs.insert("limit", self.limit.unwrap_or(100).to_string());
297        qs.insert("tags", tags);
298
299        query_api(client, qs).await
300    }
301}
302
303/// Tag on Gelbooru
304#[derive(Deserialize, Debug)]
305pub struct Tag {
306    pub id: u64,
307    pub name: String,
308    pub count: u64,
309    #[serde(rename = "type")]
310    pub tag_type: u64,
311    pub ambiguous: u64,
312}
313
314impl ApiQuery for TagQuery {}
315
316impl Tag {
317    pub fn id(&self) -> u64 {
318        self.id
319    }
320
321    #[deprecated(since="0.3.5", note="Use tag.name() instead")]
322    pub fn tag<'a>(&'a self) -> &'a str {
323        &self.name()
324    }
325
326    pub fn name<'a>(&'a self) -> &'a str {
327        &self.name
328    }
329
330    pub fn count(&self) -> u64 {
331        self.count
332    }
333
334    pub fn tag_type(&self) -> TagType {
335        use TagType::*;
336        match self.tag_type {
337            1 => Artist,
338            4 => Character,
339            3 => Copyright,
340            2 => Deprecated,
341            5 => Metadata,
342            0 => Tag,
343            _ => unreachable!("non-standard tag type"),
344        }
345    }
346
347    #[deprecated(since="0.3.5", note="Use tag.ambiguous() instead")]
348    pub fn ambigious(&self) -> bool {
349        self.ambiguous()
350    }
351
352    pub fn ambiguous(&self) -> bool {
353        if self.ambiguous == 0 {
354            false
355        } else {
356            true
357        }
358    }
359}
360
361/// The type of a tag.
362#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
363pub enum TagType {
364    Artist,
365    Character,
366    Copyright,
367    Deprecated,
368    Metadata,
369    Tag,
370}
371
372/// Determines what field sorts tags in a query.
373#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
374pub enum Ordering {
375    Date,
376    Count,
377    Name,
378}
379
380/// Request builder for the Tags endpoint.
381///
382/// See the [`tags`](fn.tags.html) function for proper usage.
383#[derive(Clone, Debug)]
384pub struct TagsRequestBuilder {
385    limit: Option<usize>,
386    after_id: Option<usize>,
387    order_by: Option<Ordering>,
388    ascending: Option<bool>,
389}
390
391enum TagSearch<'a> {
392    Name(&'a str),
393    Names(Vec<&'a str>),
394    Pattern(&'a str),
395}
396
397impl TagsRequestBuilder {
398    pub(crate) fn new() -> Self {
399        Self {
400            limit: None,
401            after_id: None,
402            order_by: None,
403            ascending: None,
404        }
405    }
406
407    /// Amount of tags to recieve.
408    ///
409    /// When unspecified, default limit is 100, as set by the server.
410    pub fn limit(mut self, limit: usize) -> Self {
411        self.limit = Some(limit);
412        self
413    }
414
415    pub fn after_id(mut self, id: usize) -> Self {
416        self.after_id = Some(id);
417        self
418    }
419
420    pub fn ascending(mut self, ascending: bool) -> Self {
421        self.ascending = Some(ascending);
422        self
423    }
424
425    /// How tags are sorted.
426    /// _Date, Count, Name._
427    ///
428    /// This is mainly useful with [`send`](#method.send), but ordering works with the other search
429    /// methods as well ([`name`](#method.name), [`names`](#method.names), [`pattern`](#method.pattern))
430    ///
431    /// ## Example
432    /// ```rust
433    /// # use gelbooru_api::{Client, Error, Ordering, tags};
434    /// # async fn example() -> Result<(), Error> {
435    /// # let client = Client::public();
436    /// tags()
437    ///     .limit(5)                 // 5 tags
438    ///     .ascending(true)             // descending
439    ///     .order_by(Ordering::Date) // according to creation-time
440    ///     .send(&client)
441    ///     .await?;
442    /// # Ok(())
443    /// # }
444    /// ```
445    pub fn order_by(mut self, ordering: Ordering) -> Self {
446        self.order_by = Some(ordering);
447        self
448    }
449
450    /// Query for tags without name/pattern specifier.
451    ///
452    /// ## Example
453    /// ```rust
454    /// # use gelbooru_api::{Client, Error, Ordering, tags};
455    /// # async fn example() -> Result<(), Error> {
456    /// # let client = Client::public();
457    /// tags()
458    ///     .limit(10)                 // 10 tags
459    ///     .ascending(false)             // descending
460    ///     .order_by(Ordering::Count) // according to count (how many posts have tag)
461    ///     .send(&client)
462    ///     .await?;
463    /// # Ok(())
464    /// # }
465    /// ```
466    pub async fn send(self, client: &Client) -> Result<TagQuery, Error> {
467        self.search(client, None).await
468    }
469
470    /// Pull data for given tag
471    ///
472    /// ## Example
473    /// ```rust
474    /// # use gelbooru_api::{Client, Error, Ordering, tags};
475    /// # async fn example() -> Result<(), Error> {
476    /// # let client = Client::public();
477    /// tags()
478    ///     .name(&client, "solo")
479    ///     .await?;
480    /// # Ok(())
481    /// # }
482    pub async fn name<S: AsRef<str>>(self, client: &Client, name: S) -> Result<Option<Tag>, Error> {
483        let search = TagSearch::Name(name.as_ref());
484        self.search(client, Some(search))
485            .await
486            .map(|tags| tags.tags.into_iter().next())
487    }
488
489    /// Pull data for the specified tags
490    ///
491    /// Tag limit is automatically set to accompany all the names.
492    ///
493    /// ## Example
494    /// ```rust
495    /// # use gelbooru_api::{Client, Error, Ordering, tags};
496    /// # async fn example() -> Result<(), Error> {
497    /// # let client = Client::public();
498    /// tags()
499    ///     .names(&client, &["solo", "hatsune_miku"])
500    ///     .await?;
501    /// # Ok(())
502    /// # }
503    pub async fn names<S: AsRef<str>>(
504        self,
505        client: &Client,
506        names: &[S],
507    ) -> Result<TagQuery, Error> {
508        let names = names.iter().map(|name| name.as_ref()).collect();
509        let search = TagSearch::Names(names);
510        self.search(client, Some(search)).await
511    }
512
513    /// Search for tags with a pattern.
514    ///
515    /// Use `_` for single-character wildcards and `%` for multi-character wildcards.
516    /// (`%choolgirl%` would act as `*choolgirl*` wildcard search)
517    ///
518    /// ## Example
519    /// ```rust
520    /// # use gelbooru_api::{Client, Error, Ordering, tags};
521    /// # async fn example() -> Result<(), Error> {
522    /// # let client = Client::public();
523    /// tags()
524    ///     .limit(10)
525    ///     .pattern(&client, "%o_o") // matches regex /.*o.o/
526    ///     .await?;
527    /// # Ok(())
528    /// # }
529    pub async fn pattern<S: AsRef<str>>(
530        self,
531        client: &Client,
532        pattern: S,
533    ) -> Result<TagQuery, Error> {
534        let search = TagSearch::Pattern(pattern.as_ref());
535        self.search(client, Some(search)).await
536    }
537
538    async fn search(
539        self,
540        client: &Client,
541        search: Option<TagSearch<'_>>,
542    ) -> Result<TagQuery, Error> {
543        let limit = self.limit.unwrap_or_else(|| {
544            use TagSearch::*;
545            match &search {
546                Some(Name(_)) => 1,
547                Some(Names(names)) => names.len(),
548                _ => 100,
549            }
550        });
551
552        let mut qs: QueryStrings = Default::default();
553        qs.insert("s", "tag".to_string());
554        qs.insert("limit", limit.to_string());
555
556        if let Some(id) = self.after_id {
557            qs.insert("after_id", id.to_string());
558        }
559
560        if let Some(ordering) = self.order_by {
561            use Ordering::*;
562            let order_by = match ordering {
563                Date => "date",
564                Count => "count",
565                Name => "name",
566            }
567            .to_string();
568            qs.insert("orderby", order_by);
569        }
570
571        if let Some(ascending) = self.ascending {
572            qs.insert("order", if ascending { "ASC" } else { "DESC" }.to_string());
573        }
574
575        if let Some(search) = search {
576            use TagSearch::*;
577            let (mode, mode_value) = match search {
578                Name(name) => ("name", name.to_string()),
579                Names(names) => ("names", names.join("+")),
580                Pattern(pattern) => ("name_pattern", pattern.to_string()),
581            };
582            qs.insert(mode, mode_value);
583        }
584
585        query_api(client, qs).await
586    }
587}
588
589/*
590 * @TODO: add support for reading XML, since Comments & Deleted Images APIs don't support
591 * outputting in json.
592
593#[derive(Deserialize, Debug)]
594pub struct Comment {}
595
596impl ApiType for Comment {}
597
598pub async fn comments(client: &Client, post_id: u64) -> Result<Vec<Comment>, Error> {
599        let mut qs: QueryStrings = Default::default();
600        qs.insert("s", "comment".to_string());
601        qs.insert("post_id", post_id.to_string());
602
603        query_api(client, qs).await
604}
605*/
606
607// internal function as to DRY
608async fn query_api<T: ApiQuery>(client: &Client, mut qs: QueryStrings<'_>) -> Result<T, Error> {
609    if let Some(auth) = &client.auth {
610        qs.insert("user_id", auth.user.to_string());
611        qs.insert("api_key", auth.key.clone());
612    }
613
614    let query_string: String = qs
615        .iter()
616        .map(|(query, value)| format!("&{}={}", query, value))
617        .collect();
618
619    let uri = format!("{}{}", API_BASE, query_string)
620        .parse::<hyper::Uri>()
621        .map_err(|err| Error::UriParse(err))?;
622
623    let res = client
624        .http_client
625        .get(uri)
626        .await
627        .map_err(|err| Error::Request(err))?;
628    let body = hyper::body::aggregate(res)
629        .await
630        .map_err(|err| Error::Request(err))?;
631
632    serde_json::from_reader(body.reader()).map_err(|err| Error::JsonDeserialize(err))
633}