use crate::{Id};
use crate::post::{Post, RawPost, RawPosts};
use anyhow::Result;
use base64::{engine::GeneralPurpose, Engine};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
use reqwest::Response;
use std::time::{Duration, Instant};
const MAX_REQ_PER_SEC: u8 = 2;
#[derive(Clone, Debug)]
pub struct Client {
host: &'static str,
http_client: reqwest::Client,
last_request_time: Instant,
request_counter: u8
}
impl Client {
pub fn new(auth: Authentication, useragent: &str) -> Result<Self> {
let mut header_map = HeaderMap::new();
header_map.append(USER_AGENT, HeaderValue::from_str(useragent)?);
if let Authentication::Authorized { username, apikey } = auth {
let authorization = Self::get_authorization_value(username, apikey);
header_map.append(AUTHORIZATION, HeaderValue::from_str(&authorization)?);
}
let http_client = reqwest::Client::builder()
.default_headers(header_map)
.build()?;
Ok(Client {
host: "https://e621.net",
http_client,
last_request_time: Instant::now(),
request_counter: 0
})
}
pub async fn list_posts(
&mut self,
limit: Option<u16>,
tags: Option<String>,
page: Option<u32>,
) -> Result<Vec<Post>> {
self.request_limiter().await;
let res = self.list_posts_raw(limit, tags, page).await?;
res.error_for_status_ref()?;
Ok(res.json::<RawPosts>().await?.into())
}
pub async fn get_post(&mut self, post_id: Id) -> Result<Post> {
self.request_limiter().await;
let res = self.get_post_raw(post_id).await?;
res.error_for_status_ref()?;
Ok(res.json::<RawPost>().await?.into())
}
pub async fn get_post_raw(&mut self, post_id: Id) -> Result<Response> {
let url = url::Url::parse(format!("{}/posts/{}.json", self.host, post_id).as_str())?;
self.request_counter = self.request_counter+1;
Ok(self.http_client.get(url.as_str()).send().await?)
}
async fn request_limiter(&mut self) {
let wait_time = Instant::now() - self.last_request_time;
if Instant::now() - self.last_request_time > Duration::from_secs(1) {
self.last_request_time = Instant::now();
self.request_counter = 0;
return;
}
if self.request_counter >= MAX_REQ_PER_SEC {
tokio::time::sleep(wait_time).await;
self.last_request_time = Instant::now();
self.request_counter = 0;
return;
}
}
fn get_authorization_value(username: &str, apikey: &str) -> String {
let base64_engine = GeneralPurpose::new(&base64::alphabet::STANDARD, Default::default());
base64_engine.encode(format!("{username}:{apikey}"))
}
async fn list_posts_raw(
&mut self,
limit: Option<u16>,
tags: Option<String>,
page: Option<u32>,
) -> Result<Response> {
let mut url = url::Url::parse(format!("{}/posts.json", self.host).as_str())?;
let mut query_params = Vec::new();
if let Some(limit) = limit {
query_params.push(format!("limit={limit}"));
}
if let Some(page) = page {
query_params.push(format!("page={page}"));
}
if let Some(tags) = tags {
if !tags.is_empty() {
query_params.push(format!("tags={tags}"));
}
}
url.set_query(Some(&query_params.join("&")));
self.request_counter = self.request_counter+1;
Ok(self.http_client.get(url.as_str()).send().await?)
}
}
#[derive(Clone, Copy, Debug)]
pub enum Authentication<'a> {
Authorized { username: &'a str, apikey: &'a str },
Unauthorized,
}