feat(image): add brat + animated brat image generation
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:
asepharyana
2026-09-01 20:17:32 +07:00
parent 278d4fe32a
commit 4efcf1f0ea
7 changed files with 113 additions and 0 deletions
+14
View File
@@ -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
View File
@@ -1,6 +1,7 @@
pub mod anime; pub mod anime;
pub mod anime2; pub mod anime2;
pub mod downloader; pub mod downloader;
pub mod image;
pub mod komik; pub mod komik;
pub mod misc; pub mod misc;
pub mod proxy; pub mod proxy;
+46
View File
@@ -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
View File
@@ -1,5 +1,6 @@
pub mod alqanime; pub mod alqanime;
pub mod downloader; pub mod downloader;
pub mod image;
pub mod komik; pub mod komik;
pub mod misc; pub mod misc;
pub mod otakudesu; pub mod otakudesu;
+41
View File
@@ -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"))
}
+1
View File
@@ -2,6 +2,7 @@ pub mod anime;
pub mod anime2; pub mod anime2;
pub mod downloader; pub mod downloader;
pub mod health; pub mod health;
pub mod image;
pub mod komik; pub mod komik;
pub mod misc; pub mod misc;
pub mod proxy; pub mod proxy;
+9
View File
@@ -349,6 +349,15 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
"/tool/cek-resi", "/tool/cek-resi",
axum::routing::get(crate::presentation::handler::tools::cek_resi_handler), 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 // Health
.route( .route(
"/health", "/health",