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 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;
|
||||||
|
|||||||
@@ -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 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;
|
||||||
|
|||||||
@@ -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 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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user