1use crate::{Client, Error};
6use hyper::body::Buf;
7use serde::Deserialize;
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::convert::{AsRef, Into};
11
12trait 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
123pub enum Rating {
124 Safe,
125 Questionable,
126 Explicit,
127}
128
129#[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 pub fn limit(mut self, limit: usize) -> Self {
146 self.limit = Some(limit);
147 self
148 }
149
150 pub fn tag<S: Into<Cow<'a, str>>>(mut self, tag: S) -> Self {
168 self.tags.push(tag.into());
169 self
170 }
171
172 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 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 pub fn clear_tags(mut self) -> Self {
230 self.tags = Vec::new();
231 self.tags_raw = String::new();
232 self
233 }
234
235 pub fn rating(mut self, rating: Rating) -> Self {
253 self.rating = Some(rating);
254 self
255 }
256
257 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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
374pub enum Ordering {
375 Date,
376 Count,
377 Name,
378}
379
380#[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 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 pub fn order_by(mut self, ordering: Ordering) -> Self {
446 self.order_by = Some(ordering);
447 self
448 }
449
450 pub async fn send(self, client: &Client) -> Result<TagQuery, Error> {
467 self.search(client, None).await
468 }
469
470 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 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 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
589async 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}