feat: trending and categories routes
This commit is contained in:
committed by
Angelo Kontaxis
parent
a92152d86d
commit
5885e067a6
@@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration};
|
||||
use lru_time_cache::LruCache;
|
||||
use reqwest::Client;
|
||||
use revolt_coalesced::{CoalescionService, CoalescionServiceConfig};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::sync::RwLock;
|
||||
use urlencoding::encode as url_encode;
|
||||
|
||||
@@ -19,34 +20,89 @@ pub enum TenorError {
|
||||
pub struct Tenor {
|
||||
pub key: Arc<str>,
|
||||
pub client: Client,
|
||||
pub coalescion: CoalescionService<String, Result<Arc<types::SearchResponse>, TenorError>>,
|
||||
pub cache: Arc<RwLock<LruCache<String, Arc<types::SearchResponse>>>>,
|
||||
|
||||
pub cache: Arc<RwLock<LruCache<String, Arc<types::PaginatedMediaResponse>>>>,
|
||||
pub cache_coalescion:
|
||||
CoalescionService<String, Result<Arc<types::PaginatedMediaResponse>, TenorError>>,
|
||||
|
||||
pub categories: Arc<RwLock<LruCache<String, Arc<types::CategoriesResponse>>>>,
|
||||
pub categories_coalescion:
|
||||
CoalescionService<String, Result<Arc<types::CategoriesResponse>, TenorError>>,
|
||||
|
||||
pub featured: Arc<RwLock<LruCache<String, Arc<types::PaginatedMediaResponse>>>>,
|
||||
pub featured_coalescion:
|
||||
CoalescionService<String, Result<Arc<types::PaginatedMediaResponse>, TenorError>>,
|
||||
}
|
||||
|
||||
impl Tenor {
|
||||
pub fn new(key: &str) -> Self {
|
||||
// 1 hour, 1k queries
|
||||
let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60 * 60), 1000);
|
||||
|
||||
Self {
|
||||
key: Arc::from(key),
|
||||
client: Client::new(),
|
||||
coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||
|
||||
// 1 hour, 1k requests
|
||||
cache: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(60 * 60),
|
||||
1000,
|
||||
))),
|
||||
cache_coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||
max_concurrent: Some(100),
|
||||
queue_requests: true,
|
||||
max_queue: None,
|
||||
}),
|
||||
|
||||
// 1 day, 1k requests
|
||||
categories: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(60 * 60 * 24),
|
||||
1000,
|
||||
))),
|
||||
categories_coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||
max_concurrent: Some(100),
|
||||
queue_requests: true,
|
||||
max_queue: None,
|
||||
}),
|
||||
|
||||
// 1 day, 1k requests
|
||||
featured: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(60 * 60 * 24),
|
||||
1000,
|
||||
))),
|
||||
featured_coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||
max_concurrent: Some(100),
|
||||
queue_requests: true,
|
||||
max_queue: None,
|
||||
}),
|
||||
cache: Arc::new(RwLock::new(cache)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request<T: DeserializeOwned>(&self, path: String) -> Result<Arc<T>, TenorError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(format!("{TENOR_API_BASE_URL}{path}"))
|
||||
.send()
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
revolt_config::capture_error(e);
|
||||
})
|
||||
.map_err(|_| TenorError::HttpError)?;
|
||||
|
||||
let text = response.text().await.map_err(|e| {
|
||||
println!("{e:?}");
|
||||
TenorError::HttpError
|
||||
})?;
|
||||
|
||||
Ok(Arc::new(serde_json::from_str(&text).unwrap()))
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
&self,
|
||||
query: &str,
|
||||
locale: &str,
|
||||
position: Option<&str>,
|
||||
) -> Result<Arc<types::SearchResponse>, TenorError> {
|
||||
let unique_key = format!("{query}:{locale}:{position:?}");
|
||||
limit: u32,
|
||||
is_category: bool,
|
||||
position: &str,
|
||||
) -> Result<Arc<types::PaginatedMediaResponse>, TenorError> {
|
||||
let unique_key = format!("s:{query}:{locale}:{is_category}:{position}");
|
||||
|
||||
if self.cache.read().await.contains_key(&unique_key) {
|
||||
if let Some(response) = self.cache.write().await.get(&unique_key) {
|
||||
@@ -54,32 +110,24 @@ impl Tenor {
|
||||
}
|
||||
}
|
||||
|
||||
let res = self.coalescion.execute(unique_key.clone(), || {
|
||||
let client = self.client.clone();
|
||||
let res = self.cache_coalescion.execute(unique_key.clone(), || {
|
||||
let mut path = format!(
|
||||
"/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}",
|
||||
&self.key,
|
||||
url_encode(query),
|
||||
url_encode(locale),
|
||||
);
|
||||
|
||||
async move {
|
||||
let mut url = format!(
|
||||
"{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=medium&limit=1",
|
||||
&self.key,
|
||||
url_encode(query),
|
||||
url_encode(locale),
|
||||
);
|
||||
if !position.is_empty() {
|
||||
path.push_str("&pos=");
|
||||
path.push_str(position);
|
||||
};
|
||||
|
||||
if let Some(position) = position {
|
||||
url.push_str("&pos=");
|
||||
url.push_str(position);
|
||||
};
|
||||
|
||||
let response = client.get(url)
|
||||
.send()
|
||||
.await
|
||||
.inspect_err(|e| { revolt_config::capture_error(e); })
|
||||
.map_err(|_| TenorError::HttpError)?;
|
||||
|
||||
let text = response.text().await.map_err(|e| { println!("{e:?}"); TenorError::HttpError })?;
|
||||
|
||||
Ok(Arc::new(serde_json::from_str(&text).unwrap()))
|
||||
if is_category {
|
||||
path.push_str("&component=categories");
|
||||
}
|
||||
|
||||
self.request(path)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -90,4 +138,81 @@ impl Tenor {
|
||||
|
||||
(*res).clone()
|
||||
}
|
||||
|
||||
pub async fn categories(
|
||||
&self,
|
||||
locale: &str,
|
||||
) -> Result<Arc<types::CategoriesResponse>, TenorError> {
|
||||
let unique_key = format!("c-{locale}");
|
||||
|
||||
if self.categories.read().await.contains_key(&unique_key) {
|
||||
if let Some(response) = self.categories.write().await.get(&unique_key) {
|
||||
return Ok(response.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let res = self
|
||||
.categories_coalescion
|
||||
.execute(unique_key.clone(), || {
|
||||
let path = format!(
|
||||
"/categories?key={}&client_key=Gifbox&locale={}&contentfilter=high",
|
||||
&self.key,
|
||||
url_encode(locale),
|
||||
);
|
||||
|
||||
self.request(path)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(resp) = &*res {
|
||||
self.categories
|
||||
.write()
|
||||
.await
|
||||
.insert(unique_key, resp.clone());
|
||||
}
|
||||
|
||||
(*res).clone()
|
||||
}
|
||||
|
||||
pub async fn featured(
|
||||
&self,
|
||||
locale: &str,
|
||||
limit: u32,
|
||||
position: &str,
|
||||
) -> Result<Arc<types::PaginatedMediaResponse>, TenorError> {
|
||||
let unique_key = format!("f-{locale}-{limit}-{position}");
|
||||
|
||||
if self.categories.read().await.contains_key(&unique_key) {
|
||||
if let Some(response) = self.featured.write().await.get(&unique_key) {
|
||||
return Ok(response.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let res = self.featured_coalescion.execute(unique_key.clone(), || {
|
||||
let mut path = format!(
|
||||
"/featured?key={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}",
|
||||
&self.key,
|
||||
url_encode(locale),
|
||||
);
|
||||
|
||||
if !position.is_empty() {
|
||||
path.push_str("&pos=");
|
||||
path.push_str(position);
|
||||
};
|
||||
|
||||
self.request(path)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(resp) = &*res {
|
||||
self.featured
|
||||
.write()
|
||||
.await
|
||||
.insert(unique_key, resp.clone());
|
||||
}
|
||||
|
||||
(*res).clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct SearchResponse {
|
||||
pub results: Vec<MediaResult>,
|
||||
pub struct PaginatedMediaResponse {
|
||||
pub results: Vec<MediaResponse>,
|
||||
pub next: String,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct MediaObject {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct MediaResult {
|
||||
pub struct MediaResponse {
|
||||
pub created: f64,
|
||||
#[serde(default)]
|
||||
pub hasaudio: bool,
|
||||
@@ -33,3 +33,17 @@ pub struct MediaResult {
|
||||
pub bg_color: Option<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct CategoriesResponse {
|
||||
pub locale: String,
|
||||
pub tags: Vec<CategoryResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct CategoryResponse {
|
||||
pub searchterm: String,
|
||||
pub path: String,
|
||||
pub image: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user