use crate::{Client, Error};
use hyper::body::Buf;
use serde::Deserialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::convert::{AsRef, Into};
trait ApiQuery: serde::de::DeserializeOwned {}
const API_BASE: &'static str = "https://gelbooru.com/index.php?page=dapi&q=index&json=1";
type QueryStrings<'a> = HashMap<&'a str, String>;
#[derive(Deserialize, Debug)]
pub struct Attributes {
pub limit: usize,
pub offset: usize,
pub count: usize,
}
#[derive(Deserialize, Debug)]
pub struct PostQuery {
#[serde(rename = "@attributes")]
pub attributes: Attributes,
#[serde(rename = "post", default = "Vec::new")]
pub posts: Vec<Post>,
}
#[derive(Deserialize, Debug)]
pub struct TagQuery {
#[serde(rename = "@attributes")]
pub attributes: Attributes,
#[serde(rename = "tag", default = "Vec::new")]
pub tags: Vec<Tag>,
}
#[derive(Deserialize, Debug)]
pub struct Post {
pub source: String,
pub directory: String,
pub height: u64,
pub id: u64,
pub image: String,
pub change: u64,
pub owner: String,
pub parent_id: Option<u64>,
pub rating: String,
pub sample: u64,
pub preview_height: u64,
pub preview_width: u64,
pub sample_height: u64,
pub sample_width: u64,
pub score: u64,
pub tags: String,
pub title: String,
pub width: u64,
pub file_url: String,
pub created_at: String,
pub post_locked: u64,
}
impl ApiQuery for PostQuery {}
impl Post {
pub fn id(&self) -> u64 {
self.id
}
pub fn title<'a>(&'a self) -> &'a str {
&self.title
}
pub fn score(&self) -> u64 {
self.score
}
pub fn created_at(&self) -> chrono::DateTime<chrono::offset::FixedOffset> {
chrono::DateTime::parse_from_str(&self.created_at, "%a %b %d %H:%M:%S %z %Y")
.expect("failed to parse DateTime")
}
pub fn rating<'a>(&'a self) -> Rating {
use crate::Rating::*;
match &self.rating[0..1] {
"s" => Safe,
"q" => Questionable,
"e" => Explicit,
_ => unreachable!("non-standard rating"),
}
}
pub fn owner<'a>(&'a self) -> &'a str {
&self.owner
}
pub fn tags<'a>(&'a self) -> Vec<&'a str> {
self.tags.split(' ').collect()
}
pub fn dimensions(&self) -> (u64, u64) {
(self.width, self.height)
}
pub fn image_url<'a>(&'a self) -> &'a str {
&self.file_url
}
pub fn source<'a>(&'a self) -> &'a str {
&self.source
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Rating {
Safe,
Questionable,
Explicit,
}
#[derive(Clone, Debug)]
pub struct PostsRequestBuilder<'a> {
pub(crate) limit: Option<usize>,
pub(crate) tags: Vec<Cow<'a, str>>,
pub(crate) tags_raw: String,
pub(crate) rating: Option<Rating>,
pub(crate) sort_random: bool,
}
impl<'a> PostsRequestBuilder<'a> {
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn tag<S: Into<Cow<'a, str>>>(mut self, tag: S) -> Self {
self.tags.push(tag.into());
self
}
pub fn tags<S: AsRef<str>>(mut self, tags: &'a [S]) -> Self {
let mut other = tags
.iter()
.map(|s| Cow::from(s.as_ref()))
.collect::<Vec<_>>();
self.tags.append(&mut other);
self
}
pub fn tags_raw<S: std::string::ToString>(mut self, raw_tags: S) -> Self {
self.tags_raw = raw_tags.to_string();
self
}
pub fn clear_tags(mut self) -> Self {
self.tags = Vec::new();
self.tags_raw = String::new();
self
}
pub fn rating(mut self, rating: Rating) -> Self {
self.rating = Some(rating);
self
}
pub fn random(mut self, random: bool) -> Self {
self.sort_random = random;
self
}
pub async fn send(self, client: &Client) -> Result<PostQuery, Error> {
let mut tags = String::new();
if let Some(rating) = self.rating {
tags.push_str(&format!("rating:{:?}+", rating).to_lowercase());
}
if self.sort_random {
tags.push_str("sort:random+");
}
tags.push_str(&self.tags.join("+"));
if !self.tags_raw.is_empty() {
tags.push('+');
tags.push_str(&self.tags_raw);
}
let mut qs: QueryStrings = Default::default();
qs.insert("s", "post".to_string());
qs.insert("limit", self.limit.unwrap_or(100).to_string());
qs.insert("tags", tags);
query_api(client, qs).await
}
}
#[derive(Deserialize, Debug)]
pub struct Tag {
pub id: u64,
pub name: String,
pub count: u64,
#[serde(rename = "type")]
pub tag_type: u64,
pub ambiguous: u64,
}
impl ApiQuery for TagQuery {}
impl Tag {
pub fn id(&self) -> u64 {
self.id
}
#[deprecated(since="0.3.5", note="Use tag.name() instead")]
pub fn tag<'a>(&'a self) -> &'a str {
&self.name()
}
pub fn name<'a>(&'a self) -> &'a str {
&self.name
}
pub fn count(&self) -> u64 {
self.count
}
pub fn tag_type(&self) -> TagType {
use TagType::*;
match self.tag_type {
1 => Artist,
4 => Character,
3 => Copyright,
2 => Deprecated,
5 => Metadata,
0 => Tag,
_ => unreachable!("non-standard tag type"),
}
}
#[deprecated(since="0.3.5", note="Use tag.ambiguous() instead")]
pub fn ambigious(&self) -> bool {
self.ambiguous()
}
pub fn ambiguous(&self) -> bool {
if self.ambiguous == 0 {
false
} else {
true
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum TagType {
Artist,
Character,
Copyright,
Deprecated,
Metadata,
Tag,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Ordering {
Date,
Count,
Name,
}
#[derive(Clone, Debug)]
pub struct TagsRequestBuilder {
limit: Option<usize>,
after_id: Option<usize>,
order_by: Option<Ordering>,
ascending: Option<bool>,
}
enum TagSearch<'a> {
Name(&'a str),
Names(Vec<&'a str>),
Pattern(&'a str),
}
impl TagsRequestBuilder {
pub(crate) fn new() -> Self {
Self {
limit: None,
after_id: None,
order_by: None,
ascending: None,
}
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn after_id(mut self, id: usize) -> Self {
self.after_id = Some(id);
self
}
pub fn ascending(mut self, ascending: bool) -> Self {
self.ascending = Some(ascending);
self
}
pub fn order_by(mut self, ordering: Ordering) -> Self {
self.order_by = Some(ordering);
self
}
pub async fn send(self, client: &Client) -> Result<TagQuery, Error> {
self.search(client, None).await
}
pub async fn name<S: AsRef<str>>(self, client: &Client, name: S) -> Result<Option<Tag>, Error> {
let search = TagSearch::Name(name.as_ref());
self.search(client, Some(search))
.await
.map(|tags| tags.tags.into_iter().next())
}
pub async fn names<S: AsRef<str>>(
self,
client: &Client,
names: &[S],
) -> Result<TagQuery, Error> {
let names = names.iter().map(|name| name.as_ref()).collect();
let search = TagSearch::Names(names);
self.search(client, Some(search)).await
}
pub async fn pattern<S: AsRef<str>>(
self,
client: &Client,
pattern: S,
) -> Result<TagQuery, Error> {
let search = TagSearch::Pattern(pattern.as_ref());
self.search(client, Some(search)).await
}
async fn search(
self,
client: &Client,
search: Option<TagSearch<'_>>,
) -> Result<TagQuery, Error> {
let limit = self.limit.unwrap_or_else(|| {
use TagSearch::*;
match &search {
Some(Name(_)) => 1,
Some(Names(names)) => names.len(),
_ => 100,
}
});
let mut qs: QueryStrings = Default::default();
qs.insert("s", "tag".to_string());
qs.insert("limit", limit.to_string());
if let Some(id) = self.after_id {
qs.insert("after_id", id.to_string());
}
if let Some(ordering) = self.order_by {
use Ordering::*;
let order_by = match ordering {
Date => "date",
Count => "count",
Name => "name",
}
.to_string();
qs.insert("orderby", order_by);
}
if let Some(ascending) = self.ascending {
qs.insert("order", if ascending { "ASC" } else { "DESC" }.to_string());
}
if let Some(search) = search {
use TagSearch::*;
let (mode, mode_value) = match search {
Name(name) => ("name", name.to_string()),
Names(names) => ("names", names.join("+")),
Pattern(pattern) => ("name_pattern", pattern.to_string()),
};
qs.insert(mode, mode_value);
}
query_api(client, qs).await
}
}
async fn query_api<T: ApiQuery>(client: &Client, mut qs: QueryStrings<'_>) -> Result<T, Error> {
if let Some(auth) = &client.auth {
qs.insert("user_id", auth.user.to_string());
qs.insert("api_key", auth.key.clone());
}
let query_string: String = qs
.iter()
.map(|(query, value)| format!("&{}={}", query, value))
.collect();
let uri = format!("{}{}", API_BASE, query_string)
.parse::<hyper::Uri>()
.map_err(|err| Error::UriParse(err))?;
let res = client
.http_client
.get(uri)
.await
.map_err(|err| Error::Request(err))?;
let body = hyper::body::aggregate(res)
.await
.map_err(|err| Error::Request(err))?;
serde_json::from_reader(body.reader()).map_err(|err| Error::JsonDeserialize(err))
}