feat(downloader): add 26 downloader APIs + detect endpoint
Deploy Scraper / build-and-deploy (push) Canceled after 0s

Port downloaders from Shirokami-API (Node.js):
- Instagram/Facebook (snapsave.app, downr.org fallback)
- TikTok (tikwm.com)
- YouTube MP4/MP3 (savetube.media, ydlp.yard.id)
- Spotify, Twitter/X (api.lrm.tube)
- Bilibili, Pinterest, MediaFire, Mega, TeraBox
- PixelDrain, Threads, DoodStream, KrakenFiles
- Danbooru, SoundCloud, Google Drive

Domain layer: DownloadResult + MediaItem entities
Application layer: 7 use-cases + detect_platform auto-detect
Infrastructure: DownloaderRepository (22 fetch functions)
Presentation: 21 handlers + DTO + OpenAPI schemas

Also: upgrade OpenTelemetry to 0.28 (axum 0.7 conflict resolution)
This commit is contained in:
asepharyana
2026-09-01 00:54:06 +07:00
parent e6d75edf33
commit 763a641593
15 changed files with 3166 additions and 181 deletions
+53
View File
@@ -0,0 +1,53 @@
//! Presentation DTOs for media downloader endpoints.
use serde::Serialize;
use utoipa::ToSchema;
use crate::domain::entity::downloader::DownloadResult;
/// Standard response wrapper for all downloader endpoints.
///
/// Mirrors the Shirokami-API pattern of `{ success, ...data }` but
/// adds `status` and `message` for consistency with the anime/komik modules.
#[derive(Serialize, Debug, Clone, ToSchema)]
pub struct DownloadResponse {
pub success: bool,
pub status: i16,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
pub data: Option<DownloadResult>,
}
impl From<DownloadResult> for DownloadResponse {
fn from(data: DownloadResult) -> Self {
let success = data.status == crate::domain::entity::downloader::DownloadStatus::Success;
Self {
success,
status: if success { 200 } else { 400 },
message: data.message.clone(),
data: Some(data),
}
}
}
impl DownloadResponse {
pub fn ok(data: DownloadResult) -> Self {
let success = data.status == crate::domain::entity::downloader::DownloadStatus::Success;
let msg = data.message.clone();
Self {
success,
status: 200,
message: msg,
data: Some(data),
}
}
pub fn error(status: i16, message: impl Into<String>) -> Self {
Self {
success: false,
status,
message: Some(message.into()),
data: None,
}
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod common;
pub mod downloader;
pub mod komik;
+6
View File
@@ -132,3 +132,9 @@ impl IntoResponse for AppError {
(status, body).into_response()
}
}
impl From<ScrapingError> for AppError {
fn from(e: ScrapingError) -> Self {
AppError::Internal(format!("{}", e))
}
}
+204
View File
@@ -0,0 +1,204 @@
//! HTTP handlers for media download endpoints.
//!
//! Each handler wraps an application-layer use case, parses request params,
//! and returns a JSON response. Platform is auto-detected from the URL
//! via the all-in-one dispatcher.
use axum::extract::Query;
use axum::Json;
use serde::Deserialize;
use crate::application::downloader as use_cases;
use crate::presentation::dto::downloader::DownloadResponse;
use crate::presentation::error::AppError;
// ============================================================================
// DownloadParams + response
// ============================================================================
/// Request params for downloader endpoints.
#[derive(Debug, Deserialize)]
pub struct DownloadParams {
pub url: String,
pub cookies: Option<String>,
pub api_key: Option<String>,
pub quality: Option<String>,
}
// ============================================================================
// Handlers
// ============================================================================
/// All-in-one downloader with auto-detection.
pub async fn download(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_all_in_one(&params.url, params.cookies.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Platform detection endpoint.
pub async fn detect_platform_handler(
Query(params): Query<DownloadParams>,
) -> Result<Json<serde_json::Value>, AppError> {
let platform = use_cases::detect_platform(&params.url);
Ok(Json(serde_json::json!({
"platform": platform,
"url": params.url,
})))
}
/// Generate handler for each downloader platform.
/// Handler for _instagram.
pub async fn download_instagram(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_instagram(&params.url, params.cookies.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _facebook.
pub async fn download_facebook(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_facebook(&params.url, params.cookies.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _tiktok.
pub async fn download_tiktok(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_tiktok(&params.url, params.cookies.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _youtube.
pub async fn download_youtube(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result =
use_cases::download_youtube(&params.url, params.quality.as_deref().unwrap_or("720"))
.await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _youtube_mp3.
pub async fn download_youtube_mp3(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_youtube_mp3(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _spotify.
pub async fn download_spotify(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_spotify(&params.url, params.api_key.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _twitter.
pub async fn download_twitter(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_twitter(&params.url, params.cookies.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _pinterest.
pub async fn download_pinterest(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_pinterest(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _mega.
pub async fn download_mega(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_mega(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _terabox.
pub async fn download_terabox(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_terabox(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _gdrive.
pub async fn download_gdrive(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_gdrive(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _mediafire.
pub async fn download_mediafire(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_mediafire(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _pixeldrain.
pub async fn download_pixeldrain(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_pixeldrain(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _threads.
pub async fn download_threads(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_threads(&params.url, params.cookies.as_deref()).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _doodstream.
pub async fn download_doodstream(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_doodstream(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _krakenfiles.
pub async fn download_krakenfiles(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_krakenfiles(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _danbooru.
pub async fn download_danbooru(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_danbooru(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _soundcloud.
pub async fn download_soundcloud(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_soundcloud(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _bilibili.
pub async fn download_bilibili(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_bilibili(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
+1
View File
@@ -1,5 +1,6 @@
pub mod anime;
pub mod anime2;
pub mod downloader;
pub mod health;
pub mod komik;
pub mod proxy;
+85
View File
@@ -155,6 +155,91 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
)
// Proxy routes
// (all proxy routes removed)
// Downloader routes
.route(
"/download",
axum::routing::get(crate::presentation::handler::downloader::download),
)
.route(
"/download/detect",
axum::routing::get(crate::presentation::handler::downloader::detect_platform_handler),
)
.route(
"/download/instagram",
axum::routing::get(crate::presentation::handler::downloader::download_instagram),
)
.route(
"/download/facebook",
axum::routing::get(crate::presentation::handler::downloader::download_facebook),
)
.route(
"/download/tiktok",
axum::routing::get(crate::presentation::handler::downloader::download_tiktok),
)
.route(
"/download/youtube",
axum::routing::get(crate::presentation::handler::downloader::download_youtube),
)
.route(
"/download/youtube/mp3",
axum::routing::get(crate::presentation::handler::downloader::download_youtube_mp3),
)
.route(
"/download/spotify",
axum::routing::get(crate::presentation::handler::downloader::download_spotify),
)
.route(
"/download/twitter",
axum::routing::get(crate::presentation::handler::downloader::download_twitter),
)
.route(
"/download/pinterest",
axum::routing::get(crate::presentation::handler::downloader::download_pinterest),
)
.route(
"/download/mega",
axum::routing::get(crate::presentation::handler::downloader::download_mega),
)
.route(
"/download/terabox",
axum::routing::get(crate::presentation::handler::downloader::download_terabox),
)
.route(
"/download/gdrive",
axum::routing::get(crate::presentation::handler::downloader::download_gdrive),
)
.route(
"/download/mediafire",
axum::routing::get(crate::presentation::handler::downloader::download_mediafire),
)
.route(
"/download/pixeldrain",
axum::routing::get(crate::presentation::handler::downloader::download_pixeldrain),
)
.route(
"/download/threads",
axum::routing::get(crate::presentation::handler::downloader::download_threads),
)
.route(
"/download/dood",
axum::routing::get(crate::presentation::handler::downloader::download_doodstream),
)
.route(
"/download/kraken",
axum::routing::get(crate::presentation::handler::downloader::download_krakenfiles),
)
.route(
"/download/danbooru",
axum::routing::get(crate::presentation::handler::downloader::download_danbooru),
)
.route(
"/download/soundcloud",
axum::routing::get(crate::presentation::handler::downloader::download_soundcloud),
)
.route(
"/download/bilibili",
axum::routing::get(crate::presentation::handler::downloader::download_bilibili),
)
// Health
.route(
"/health",