feat(misc): add misc endpoints (currency-converter, harga-emas, kurs-bca, server-info)
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Ported from Shirokami-API scraper/misc/*.js: - /misc/currency-converter (wise.com scrape, amount/from/to) - /misc/harga-emas (harga-emas.org Antam table) - /misc/kurs-bca (bca.co.id jual/beli) - /misc/server-info (OS/CPU/RAM/disk from /proc) Also: remove /download/tiktok/v2 + /download/twitter/v2 as separate routes; douyin.wtf + twitsave now internal fallback steps in the primary fetch_tiktok/fetch_twitter chains (v2 = fallback per user directive).
This commit is contained in:
@@ -123,14 +123,6 @@ pub async fn download_videy(url: &str) -> Result<DownloadResult, ScrapingError>
|
|||||||
DownloaderRepository::download_videy(url).await
|
DownloaderRepository::download_videy(url).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_tiktok_v2(url: &str) -> Result<DownloadResult, ScrapingError> {
|
|
||||||
DownloaderRepository::download_tiktok_v2(url, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn download_twitter_v2(url: &str) -> Result<DownloadResult, ScrapingError> {
|
|
||||||
DownloaderRepository::download_twitter_v2(url, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn download_bilibili(url: &str) -> Result<DownloadResult, ScrapingError> {
|
pub async fn download_bilibili(url: &str) -> Result<DownloadResult, ScrapingError> {
|
||||||
DownloaderRepository::download_bilibili(url).await
|
DownloaderRepository::download_bilibili(url).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! Application use-cases for misc utilities.
|
||||||
|
|
||||||
|
use crate::domain::error::ScrapingError;
|
||||||
|
use crate::infrastructure::repository::misc as Repo;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// Currency converter.
|
||||||
|
pub async fn currency_converter(amount: f64, from: &str, to: &str) -> Result<Value, ScrapingError> {
|
||||||
|
Repo::fetch_currency_converter(amount, from, to)
|
||||||
|
.await
|
||||||
|
.map_err(ScrapingError::Http)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Harga emas Antam.
|
||||||
|
pub async fn harga_emas() -> Result<Value, ScrapingError> {
|
||||||
|
Repo::fetch_harga_emas()
|
||||||
|
.await
|
||||||
|
.map_err(ScrapingError::Http)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kurs BCA (jual/beli).
|
||||||
|
pub async fn kurs_bca() -> Result<Value, ScrapingError> {
|
||||||
|
Repo::fetch_kurs_bca()
|
||||||
|
.await
|
||||||
|
.map_err(ScrapingError::Http)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server info (OS/CPU/RAM/disk).
|
||||||
|
pub async fn server_info() -> Result<Value, ScrapingError> {
|
||||||
|
Repo::fetch_server_info()
|
||||||
|
.await
|
||||||
|
.map_err(ScrapingError::Http)
|
||||||
|
}
|
||||||
@@ -2,4 +2,5 @@ pub mod anime;
|
|||||||
pub mod anime2;
|
pub mod anime2;
|
||||||
pub mod downloader;
|
pub mod downloader;
|
||||||
pub mod komik;
|
pub mod komik;
|
||||||
|
pub mod misc;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
|
|||||||
@@ -614,28 +614,6 @@ impl DownloaderRepository {
|
|||||||
fetch_videy(url).await
|
fetch_videy(url).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TikTok v2 (douyin.wtf hybrid API; falls back to embed scrape when dead).
|
|
||||||
pub async fn download_tiktok_v2(
|
|
||||||
url: &str,
|
|
||||||
cookies: Option<&str>,
|
|
||||||
) -> Result<DownloadResult, ScrapingError> {
|
|
||||||
match fetch_tiktok_v2(url).await {
|
|
||||||
ok @ Ok(_) => ok,
|
|
||||||
Err(_) => Self::download_tiktok(url, cookies).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Twitter v2 (twitsave.com scrape; falls back to Syndication API when blocked).
|
|
||||||
pub async fn download_twitter_v2(
|
|
||||||
url: &str,
|
|
||||||
cookies: Option<&str>,
|
|
||||||
) -> Result<DownloadResult, ScrapingError> {
|
|
||||||
match fetch_twitter_v2(url).await {
|
|
||||||
ok @ Ok(_) => ok,
|
|
||||||
Err(_) => Self::download_twitter(url, cookies).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bilibili video via b23.tv short link expansion.
|
/// Bilibili video via b23.tv short link expansion.
|
||||||
pub async fn download_bilibili(url: &str) -> Result<DownloadResult, ScrapingError> {
|
pub async fn download_bilibili(url: &str) -> Result<DownloadResult, ScrapingError> {
|
||||||
fetch_bilibili(url).await
|
fetch_bilibili(url).await
|
||||||
@@ -1286,10 +1264,16 @@ pub async fn fetch_tiktok(url: &str) -> Result<DownloadResult, ScrapingError> {
|
|||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// All methods failed
|
// Last resort: douyin.wtf hybrid API (v2). If it also fails,
|
||||||
return Err(ScrapingError::Http(
|
// report all methods exhausted.
|
||||||
"All TikTok download methods failed (tikwm API blocked, yt-dlp blocked, Playwright blocked)".to_string()
|
match fetch_tiktok_v2(url).await {
|
||||||
));
|
Ok(v2_result) if !v2_result.media.is_empty() => return Ok(v2_result),
|
||||||
|
_ => {
|
||||||
|
return Err(ScrapingError::Http(
|
||||||
|
"All TikTok download methods failed (tikwm, yt-dlp, Playwright, embed, douyin.wtf)".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1951,7 +1935,15 @@ pub async fn fetch_twitter(url: &str) -> Result<DownloadResult, ScrapingError> {
|
|||||||
}
|
}
|
||||||
Err(_e) => {}
|
Err(_e) => {}
|
||||||
}
|
}
|
||||||
return Ok(DownloadResult::error("Tidak dapat menemukan video"));
|
// Last resort: twitsave.com scrape (v2). If it also fails, report.
|
||||||
|
match fetch_twitter_v2(url).await {
|
||||||
|
Ok(v2_result) if !v2_result.media.is_empty() => return Ok(v2_result),
|
||||||
|
_ => {
|
||||||
|
return Ok(DownloadResult::error(
|
||||||
|
"Tidak dapat menemukan video (savetwitter, Playwright, twitsave semua gagal)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by resolution desc
|
// Sort by resolution desc
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
//! Infrastructure — Misc utility scrapers (currency-converter, harga-emas, kurs-bca, server-info).
|
||||||
|
//!
|
||||||
|
//! Ported from Shirokami-API `scraper/misc/*.js`.
|
||||||
|
//! HTML scraping via reqwest + scraper crate.
|
||||||
|
|
||||||
|
use crate::infrastructure::utils::http_client::http_client;
|
||||||
|
use regex::Regex;
|
||||||
|
use reqwest::header::USER_AGENT;
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
/// Currency converter: scrape wise.com exchange rate page.
|
||||||
|
/// `amount`: amount to convert, `from`/`to`: currency codes (e.g. USD→IDR).
|
||||||
|
pub async fn fetch_currency_converter(amount: f64, from: &str, to: &str) -> Result<Value, String> {
|
||||||
|
let from_lower = from.to_lowercase();
|
||||||
|
let to_lower = to.to_lowercase();
|
||||||
|
let url = format!(
|
||||||
|
"https://wise.com/gb/currency-converter/{}-to-{}-rate?amount={}",
|
||||||
|
from_lower, to_lower, amount
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = http_client()
|
||||||
|
.client()
|
||||||
|
.get(&url)
|
||||||
|
.header(
|
||||||
|
USER_AGENT,
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("HTTP error: {}", e))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("HTTP {} from wise.com", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = resp
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read body: {}", e))?;
|
||||||
|
|
||||||
|
// Try regex first: "1 USD = 15,885.00 IDR"
|
||||||
|
let re = Regex::new(r"1 [A-Z]{3} = ([0-9,.]+) [A-Z]{3}")
|
||||||
|
.map_err(|e| format!("Regex compile: {}", e))?;
|
||||||
|
|
||||||
|
if let Some(caps) = re.captures(&body) {
|
||||||
|
let rate_str = caps.get(1).map(|m| m.as_str()).unwrap_or("0");
|
||||||
|
let rate = rate_str.replace(',', "").parse::<f64>().unwrap_or(0.0);
|
||||||
|
if rate > 0.0 {
|
||||||
|
let result = amount * rate;
|
||||||
|
return Ok(json!({
|
||||||
|
"from": from.to_uppercase(),
|
||||||
|
"to": to.to_uppercase(),
|
||||||
|
"amount": amount,
|
||||||
|
"rate": rate,
|
||||||
|
"result": result,
|
||||||
|
"updateTime": chrono::Utc::now().to_rfc3339(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: scrape the page for any rate info
|
||||||
|
let document = Html::parse_document(&body);
|
||||||
|
let selector = Selector::parse("body").unwrap();
|
||||||
|
let text = document
|
||||||
|
.select(&selector)
|
||||||
|
.flat_map(|e| e.text())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// Look for patterns like "15,885.00 IDR" in body text
|
||||||
|
let re2 = Regex::new(&format!(
|
||||||
|
r"([0-9,.]+)\s+{}",
|
||||||
|
to.to_uppercase()
|
||||||
|
))
|
||||||
|
.map_err(|e| format!("Regex compile: {}", e))?;
|
||||||
|
|
||||||
|
if let Some(caps) = re2.captures(&text) {
|
||||||
|
let rate_str = caps.get(1).map(|m| m.as_str()).unwrap_or("0");
|
||||||
|
let rate = rate_str.replace(',', "").parse::<f64>().unwrap_or(0.0);
|
||||||
|
if rate > 0.0 {
|
||||||
|
let result = amount * rate;
|
||||||
|
return Ok(json!({
|
||||||
|
"from": from.to_uppercase(),
|
||||||
|
"to": to.to_uppercase(),
|
||||||
|
"amount": amount,
|
||||||
|
"rate": rate,
|
||||||
|
"result": result,
|
||||||
|
"updateTime": chrono::Utc::now().to_rfc3339(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("Gagal mendapatkan kurs dari wise.com".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrape harga emas Antam from harga-emas.org.
|
||||||
|
pub async fn fetch_harga_emas() -> Result<Value, String> {
|
||||||
|
let url = "https://harga-emas.org/";
|
||||||
|
|
||||||
|
let resp = http_client()
|
||||||
|
.client()
|
||||||
|
.get(url)
|
||||||
|
.header(
|
||||||
|
USER_AGENT,
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("HTTP error: {}", e))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("HTTP {} from harga-emas.org", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = resp
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read body: {}", e))?;
|
||||||
|
|
||||||
|
let document = Html::parse_document(&body);
|
||||||
|
|
||||||
|
let mut result = json!({
|
||||||
|
"title": "Harga Emas Antam",
|
||||||
|
"hargaUtama": "N/A",
|
||||||
|
"perubahan": "0",
|
||||||
|
"detail": [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Look for tables containing "Antam" + "Pegadaian"
|
||||||
|
let table_sel = Selector::parse("table").unwrap();
|
||||||
|
let tr_sel = Selector::parse("tbody tr").unwrap();
|
||||||
|
let td_sel = Selector::parse("td").unwrap();
|
||||||
|
|
||||||
|
let mut detail: Vec<Value> = Vec::new();
|
||||||
|
|
||||||
|
for table in document.select(&table_sel) {
|
||||||
|
let table_text = table.text().collect::<String>();
|
||||||
|
if table_text.contains("Antam") && table_text.contains("Pegadaian") {
|
||||||
|
for tr in table.select(&tr_sel) {
|
||||||
|
let cols: Vec<String> = tr
|
||||||
|
.select(&td_sel)
|
||||||
|
.map(|td| td.text().collect::<String>())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if cols.len() >= 2 {
|
||||||
|
let label = cols[0].trim();
|
||||||
|
let price = cols[1].trim();
|
||||||
|
|
||||||
|
if !label.is_empty() && !price.is_empty() {
|
||||||
|
// Check if label is a numeric gram value
|
||||||
|
let numeric_label: String =
|
||||||
|
label.chars().filter(|c| c.is_ascii_digit()).collect();
|
||||||
|
if !numeric_label.is_empty() {
|
||||||
|
detail.push(json!({
|
||||||
|
"ukuran": format!("{} gr", label.replace(" gr", "").replace("gr", "").trim()),
|
||||||
|
"harga": format!("Rp {}", price),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !detail.is_empty() {
|
||||||
|
// Find the 1gr price as main price (fallback: smallest gram size).
|
||||||
|
let one_gr = detail.iter().find(|d| {
|
||||||
|
d["ukuran"]
|
||||||
|
.as_str()
|
||||||
|
.map(|s| {
|
||||||
|
let s = s.trim();
|
||||||
|
s == "1 gr" || s.starts_with("1 gr") || s.starts_with("1gr")
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
let main_price = one_gr
|
||||||
|
.map(|d| d["harga"].as_str().unwrap_or("N/A"))
|
||||||
|
.unwrap_or_else(|| detail[0]["harga"].as_str().unwrap_or("N/A"));
|
||||||
|
|
||||||
|
result["hargaUtama"] = json!(main_price);
|
||||||
|
result["detail"] = json!(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrape kurs BCA (jual/beli) from bca.co.id.
|
||||||
|
pub async fn fetch_kurs_bca() -> Result<Value, String> {
|
||||||
|
let url = "https://www.bca.co.id/id/informasi/kurs";
|
||||||
|
|
||||||
|
let resp = http_client()
|
||||||
|
.client()
|
||||||
|
.get(url)
|
||||||
|
.header(
|
||||||
|
USER_AGENT,
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
)
|
||||||
|
.header("Referer", "https://www.google.com/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("HTTP error: {}", e))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("HTTP {} from bca.co.id", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = resp
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read body: {}", e))?;
|
||||||
|
|
||||||
|
let document = Html::parse_document(&body);
|
||||||
|
|
||||||
|
let table_sel = Selector::parse("table").unwrap();
|
||||||
|
let tr_sel = Selector::parse("tbody tr").unwrap();
|
||||||
|
let td_sel = Selector::parse("td").unwrap();
|
||||||
|
|
||||||
|
let mut kurs_data: Vec<Value> = Vec::new();
|
||||||
|
|
||||||
|
for table in document.select(&table_sel) {
|
||||||
|
let table_text = table.text().collect::<String>();
|
||||||
|
if table_text.contains("USD") || table_text.contains("SGD") || table_text.contains("kurs") {
|
||||||
|
for tr in table.select(&tr_sel) {
|
||||||
|
let cols: Vec<String> = tr
|
||||||
|
.select(&td_sel)
|
||||||
|
.map(|td| td.text().collect::<String>().trim().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if cols.len() >= 3 && !cols[0].is_empty() && !cols[1].is_empty() && !cols[2].is_empty() {
|
||||||
|
kurs_data.push(json!({
|
||||||
|
"currency": cols[0],
|
||||||
|
"jual": cols[1],
|
||||||
|
"beli": cols[2],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !kurs_data.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kurs_data.is_empty() {
|
||||||
|
return Err("Gagal mendapatkan data kurs BCA".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"source": "BCA",
|
||||||
|
"url": url,
|
||||||
|
"data": kurs_data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local server info: OS, CPU, RAM, storage.
|
||||||
|
pub async fn fetch_server_info() -> Result<Value, String> {
|
||||||
|
let os_info = json!({
|
||||||
|
"platform": std::env::consts::OS,
|
||||||
|
"arch": std::env::consts::ARCH,
|
||||||
|
"family": std::env::consts::FAMILY,
|
||||||
|
});
|
||||||
|
|
||||||
|
// CPU info from /proc/cpuinfo (Linux)
|
||||||
|
let cpu_model = tokio::fs::read_to_string("/proc/cpuinfo")
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| {
|
||||||
|
s.lines()
|
||||||
|
.find(|l| l.starts_with("model name"))
|
||||||
|
.and_then(|l| l.split(':').nth(1))
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
let cpu_cores = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
// RAM from /proc/meminfo
|
||||||
|
let mem_info = tokio::fs::read_to_string("/proc/meminfo")
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let total_mem_kb: u64 = mem_info
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with("MemTotal"))
|
||||||
|
.and_then(|l| l.split_whitespace().nth(1))
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let free_mem_kb: u64 = mem_info
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with("MemAvailable"))
|
||||||
|
.and_then(|l| l.split_whitespace().nth(1))
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let total_mem_gb = total_mem_kb as f64 / 1048576.0;
|
||||||
|
let free_mem_gb = free_mem_kb as f64 / 1048576.0;
|
||||||
|
|
||||||
|
// Disk usage from `df -B1 /`
|
||||||
|
let disk_usage = tokio::process::Command::new("df")
|
||||||
|
.args(["-B1", "/"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
|
.and_then(|out| {
|
||||||
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
|
if lines.len() >= 2 {
|
||||||
|
let parts: Vec<&str> = lines[1].split_whitespace().collect();
|
||||||
|
if parts.len() >= 4 {
|
||||||
|
let total: u64 = parts[1].parse().unwrap_or(0);
|
||||||
|
let used: u64 = parts[2].parse().unwrap_or(0);
|
||||||
|
let free: u64 = parts[3].parse().unwrap_or(0);
|
||||||
|
return Some(json!({
|
||||||
|
"drive": parts[5],
|
||||||
|
"total": format_gb(total),
|
||||||
|
"used": format_gb(used),
|
||||||
|
"free": format_gb(free),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.unwrap_or(json!({
|
||||||
|
"drive": "/",
|
||||||
|
"total": "N/A",
|
||||||
|
"used": "N/A",
|
||||||
|
"free": "N/A",
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"os": os_info,
|
||||||
|
"cpu": {
|
||||||
|
"model": cpu_model,
|
||||||
|
"cores": cpu_cores,
|
||||||
|
},
|
||||||
|
"ram": {
|
||||||
|
"total": format!("{:.2} GB", total_mem_gb),
|
||||||
|
"free": format!("{:.2} GB", free_mem_gb),
|
||||||
|
"used": format!("{:.2} GB", total_mem_gb - free_mem_gb),
|
||||||
|
},
|
||||||
|
"storage": disk_usage,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_gb(bytes: u64) -> String {
|
||||||
|
let gb = bytes as f64 / 1_073_741_824.0;
|
||||||
|
format!("{:.2} GB", gb)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod alqanime;
|
pub mod alqanime;
|
||||||
pub mod downloader;
|
pub mod downloader;
|
||||||
pub mod komik;
|
pub mod komik;
|
||||||
|
pub mod misc;
|
||||||
pub mod otakudesu;
|
pub mod otakudesu;
|
||||||
pub mod parsers;
|
pub mod parsers;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
|
|||||||
@@ -227,22 +227,6 @@ pub async fn download_videy(
|
|||||||
Ok(Json(DownloadResponse::ok(result)))
|
Ok(Json(DownloadResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handler for _tiktok_v2.
|
|
||||||
pub async fn download_tiktok_v2(
|
|
||||||
Query(params): Query<DownloadParams>,
|
|
||||||
) -> Result<Json<DownloadResponse>, AppError> {
|
|
||||||
let result = use_cases::download_tiktok_v2(¶ms.url).await?;
|
|
||||||
Ok(Json(DownloadResponse::ok(result)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for _twitter_v2.
|
|
||||||
pub async fn download_twitter_v2(
|
|
||||||
Query(params): Query<DownloadParams>,
|
|
||||||
) -> Result<Json<DownloadResponse>, AppError> {
|
|
||||||
let result = use_cases::download_twitter_v2(¶ms.url).await?;
|
|
||||||
Ok(Json(DownloadResponse::ok(result)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for _bilibili.
|
/// Handler for _bilibili.
|
||||||
pub async fn download_bilibili(
|
pub async fn download_bilibili(
|
||||||
Query(params): Query<DownloadParams>,
|
Query(params): Query<DownloadParams>,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//! Axum handlers for misc utility endpoints.
|
||||||
|
//!
|
||||||
|
//! Ported from Shirokami-API `scraper/misc/*.js`.
|
||||||
|
|
||||||
|
use axum::extract::Query;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::Json;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::application::misc as use_cases;
|
||||||
|
use crate::presentation::error::AppError;
|
||||||
|
|
||||||
|
/// Query params for currency converter.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CurrencyParams {
|
||||||
|
pub amount: Option<f64>,
|
||||||
|
pub from: Option<String>,
|
||||||
|
pub to: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /misc/currency-converter?amount=100&from=USD&to=IDR
|
||||||
|
pub async fn currency_converter_handler(
|
||||||
|
Query(params): Query<CurrencyParams>,
|
||||||
|
) -> Result<(StatusCode, Json<Value>), AppError> {
|
||||||
|
let amount = params.amount.unwrap_or(1.0);
|
||||||
|
let from = params.from.unwrap_or_else(|| "USD".to_string());
|
||||||
|
let to = params.to.unwrap_or_else(|| "IDR".to_string());
|
||||||
|
|
||||||
|
let result = use_cases::currency_converter(amount, &from, &to).await?;
|
||||||
|
Ok((StatusCode::OK, Json(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /misc/harga-emas
|
||||||
|
pub async fn harga_emas_handler() -> Result<(StatusCode, Json<Value>), AppError> {
|
||||||
|
let result = use_cases::harga_emas().await?;
|
||||||
|
Ok((StatusCode::OK, Json(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /misc/kurs-bca
|
||||||
|
pub async fn kurs_bca_handler() -> Result<(StatusCode, Json<Value>), AppError> {
|
||||||
|
let result = use_cases::kurs_bca().await?;
|
||||||
|
Ok((StatusCode::OK, Json(result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /misc/server-info
|
||||||
|
pub async fn server_info_handler() -> Result<(StatusCode, Json<Value>), AppError> {
|
||||||
|
let result = use_cases::server_info().await?;
|
||||||
|
Ok((StatusCode::OK, Json(result)))
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@ pub mod anime2;
|
|||||||
pub mod downloader;
|
pub mod downloader;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod komik;
|
pub mod komik;
|
||||||
|
pub mod misc;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
|
|||||||
@@ -252,18 +252,27 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
|
|||||||
"/download/videy",
|
"/download/videy",
|
||||||
axum::routing::get(crate::presentation::handler::downloader::download_videy),
|
axum::routing::get(crate::presentation::handler::downloader::download_videy),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/download/tiktok/v2",
|
|
||||||
axum::routing::get(crate::presentation::handler::downloader::download_tiktok_v2),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/download/twitter/v2",
|
|
||||||
axum::routing::get(crate::presentation::handler::downloader::download_twitter_v2),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/download/bilibili",
|
"/download/bilibili",
|
||||||
axum::routing::get(crate::presentation::handler::downloader::download_bilibili),
|
axum::routing::get(crate::presentation::handler::downloader::download_bilibili),
|
||||||
)
|
)
|
||||||
|
// Misc routes
|
||||||
|
.route(
|
||||||
|
"/misc/currency-converter",
|
||||||
|
axum::routing::get(crate::presentation::handler::misc::currency_converter_handler),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/misc/harga-emas",
|
||||||
|
axum::routing::get(crate::presentation::handler::misc::harga_emas_handler),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/misc/kurs-bca",
|
||||||
|
axum::routing::get(crate::presentation::handler::misc::kurs_bca_handler),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/misc/server-info",
|
||||||
|
axum::routing::get(crate::presentation::handler::misc::server_info_handler),
|
||||||
|
)
|
||||||
// Health
|
// Health
|
||||||
.route(
|
.route(
|
||||||
"/health",
|
"/health",
|
||||||
|
|||||||
Reference in New Issue
Block a user