feat(image): add brat + animated brat image generation
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Ported from Shirokami-API scraper/image/brat.js (v2 path): - /image/brat - static brat PNG via brat.siputzx.my.id API - /image/brat/animated - animated brat GIF Both return raw image bytes with proper Content-Type (image/png, image/gif)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
//! Application use-cases for image generation.
|
||||
|
||||
use crate::domain::error::ScrapingError;
|
||||
use crate::infrastructure::repository::image as Repo;
|
||||
|
||||
pub async fn brat(text: &str) -> Result<Vec<u8>, ScrapingError> {
|
||||
Repo::fetch_brat(text).await.map_err(ScrapingError::Http)
|
||||
}
|
||||
|
||||
pub async fn brat_animated(text: &str) -> Result<Vec<u8>, ScrapingError> {
|
||||
Repo::fetch_brat_animated(text)
|
||||
.await
|
||||
.map_err(ScrapingError::Http)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod anime;
|
||||
pub mod anime2;
|
||||
pub mod downloader;
|
||||
pub mod image;
|
||||
pub mod komik;
|
||||
pub mod misc;
|
||||
pub mod proxy;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Infrastructure — Image generation endpoints (brat).
|
||||
//!
|
||||
//! Ported from Shirokami-API `scraper/image/brat.js`.
|
||||
//! brat v2 uses the external brat.siputzx.my.id API (returns image bytes).
|
||||
|
||||
use crate::infrastructure::utils::http_client::http_client;
|
||||
use reqwest::header::USER_AGENT;
|
||||
|
||||
const BRAT_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0 Safari/537.36";
|
||||
|
||||
fn urlencode(s: &str) -> String {
|
||||
s.replace('%', "%25")
|
||||
.replace('&', "%26")
|
||||
.replace('?', "%3F")
|
||||
}
|
||||
|
||||
/// Fetch a brat image (static) for the given text.
|
||||
pub async fn fetch_brat(text: &str) -> Result<Vec<u8>, String> {
|
||||
let url = format!("https://brat.siputzx.my.id/image?text={}", urlencode(text));
|
||||
fetch_bytes(&url).await
|
||||
}
|
||||
|
||||
/// Fetch an animated brat GIF for the given text.
|
||||
pub async fn fetch_brat_animated(text: &str) -> Result<Vec<u8>, String> {
|
||||
let url = format!("https://brat.siputzx.my.id/gif?text={}", urlencode(text));
|
||||
fetch_bytes(&url).await
|
||||
}
|
||||
|
||||
async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
|
||||
let resp = http_client()
|
||||
.client()
|
||||
.get(url)
|
||||
.header(USER_AGENT, BRAT_UA)
|
||||
.header("X-Forwarded-For", "104.28.209.35")
|
||||
.header("X-Real-IP", "104.28.209.35")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP: {}", e))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
}
|
||||
resp.bytes()
|
||||
.await
|
||||
.map(|b| b.to_vec())
|
||||
.map_err(|e| format!("Body: {}", e))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod alqanime;
|
||||
pub mod downloader;
|
||||
pub mod image;
|
||||
pub mod komik;
|
||||
pub mod misc;
|
||||
pub mod otakudesu;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Axum handlers for image generation endpoints.
|
||||
//!
|
||||
//! Returns raw image bytes with the proper Content-Type.
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::Query;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::Response;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::application::image as use_cases;
|
||||
use crate::presentation::error::AppError;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BratParams {
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn brat_handler(Query(params): Query<BratParams>) -> Result<Response, AppError> {
|
||||
let text = params
|
||||
.text
|
||||
.ok_or_else(|| AppError::BadRequest("text parameter is required".into()))?;
|
||||
let bytes = use_cases::brat(&text).await?;
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "image/png")
|
||||
.body(Body::from(bytes))
|
||||
.expect("valid response"))
|
||||
}
|
||||
|
||||
pub async fn brat_animated_handler(Query(params): Query<BratParams>) -> Result<Response, AppError> {
|
||||
let text = params
|
||||
.text
|
||||
.ok_or_else(|| AppError::BadRequest("text parameter is required".into()))?;
|
||||
let bytes = use_cases::brat_animated(&text).await?;
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "image/gif")
|
||||
.body(Body::from(bytes))
|
||||
.expect("valid response"))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub mod anime;
|
||||
pub mod anime2;
|
||||
pub mod downloader;
|
||||
pub mod health;
|
||||
pub mod image;
|
||||
pub mod komik;
|
||||
pub mod misc;
|
||||
pub mod proxy;
|
||||
|
||||
@@ -349,6 +349,15 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
|
||||
"/tool/cek-resi",
|
||||
axum::routing::get(crate::presentation::handler::tools::cek_resi_handler),
|
||||
)
|
||||
// Image routes
|
||||
.route(
|
||||
"/image/brat",
|
||||
axum::routing::get(crate::presentation::handler::image::brat_handler),
|
||||
)
|
||||
.route(
|
||||
"/image/brat/animated",
|
||||
axum::routing::get(crate::presentation::handler::image::brat_animated_handler),
|
||||
)
|
||||
// Health
|
||||
.route(
|
||||
"/health",
|
||||
|
||||
Reference in New Issue
Block a user