feat: add Dailymotion + Reddit downloader endpoints via yt-dlp
Deploy Scraper / build-and-deploy (push) Canceled after 0s

New platforms from Shirokami-style downloadter set:
- /download/dailymotion: real MP4/HLS streams (288p up to 2160p)
- /download/reddit: real v.redd.it HLS media for public posts
- detect + all-in-one dispatch updated

Both verified working from VPS with real URLs.
This commit is contained in:
asepharyana
2026-09-01 18:05:35 +07:00
parent 4dee99cc43
commit 7850b5fa03
4 changed files with 282 additions and 0 deletions
+12
View File
@@ -107,6 +107,14 @@ pub async fn download_soundcloud(url: &str) -> Result<DownloadResult, ScrapingEr
DownloaderRepository::download_soundcloud(url).await
}
pub async fn download_dailymotion(url: &str) -> Result<DownloadResult, ScrapingError> {
DownloaderRepository::download_dailymotion(url).await
}
pub async fn download_reddit(url: &str) -> Result<DownloadResult, ScrapingError> {
DownloaderRepository::download_reddit(url).await
}
pub async fn download_bilibili(url: &str) -> Result<DownloadResult, ScrapingError> {
DownloaderRepository::download_bilibili(url).await
}
@@ -147,6 +155,10 @@ pub fn detect_platform(url: &str) -> String {
"danbooru".to_string()
} else if url.contains("soundcloud.com") {
"soundcloud".to_string()
} else if url.contains("dailymotion.com") {
"dailymotion".to_string()
} else if url.contains("reddit.com") || url.contains("redd.it") {
"reddit".to_string()
} else if url.contains("bilibili.com") || url.contains("b23.tv") {
"bilibili".to_string()
} else {
+246
View File
@@ -468,6 +468,8 @@ impl DownloaderRepository {
"krakenfiles" => Self::download_krakenfiles(url).await,
"danbooru" => Self::download_danbooru(url).await,
"soundcloud" => Self::download_soundcloud(url).await,
"dailymotion" => Self::download_dailymotion(url).await,
"reddit" => Self::download_reddit(url).await,
"bilibili" => Self::download_bilibili(url).await,
_ => fetch_all_in_one(url).await,
}
@@ -590,6 +592,16 @@ impl DownloaderRepository {
fetch_soundcloud(url).await
}
/// Dailymotion video via yt-dlp.
pub async fn download_dailymotion(url: &str) -> Result<DownloadResult, ScrapingError> {
fetch_dailymotion(url).await
}
/// Reddit video via yt-dlp.
pub async fn download_reddit(url: &str) -> Result<DownloadResult, ScrapingError> {
fetch_reddit(url).await
}
/// Bilibili video via b23.tv short link expansion.
pub async fn download_bilibili(url: &str) -> Result<DownloadResult, ScrapingError> {
fetch_bilibili(url).await
@@ -2155,6 +2167,240 @@ pub async fn fetch_soundcloud(url: &str) -> Result<DownloadResult, ScrapingError
Ok(result)
}
// ============================================================================
// Dailymotion downloader (via yt-dlp)
// ============================================================================
pub async fn fetch_dailymotion(url: &str) -> Result<DownloadResult, ScrapingError> {
if !url.contains("dailymotion.com") {
return Err(ScrapingError::Http("Invalid Dailymotion URL".to_string()));
}
let data = run_ytdlp_json(url, &["-f", "bestvideo+bestaudio/best"]).await?;
let title = data
.get("title")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let author = data
.get("uploader")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let thumbnail = data
.get("thumbnail")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let duration = data
.get("duration")
.and_then(|v| v.as_u64())
.map(|d| format!("{}s", d));
let mut result = DownloadResult::success(title);
result.author = author;
result.thumbnail = thumbnail;
result.duration = duration;
result.provider = Some("yt-dlp".to_string());
let formats = data.get("formats").and_then(|v| v.as_array());
let mut seen = std::collections::HashSet::new();
if let Some(fmts) = formats {
for f in fmts {
if let (Some(furl), Some(ext_val)) = (
f.get("url").and_then(|v| v.as_str()),
f.get("ext").and_then(|v| v.as_str()),
) {
if ext_val == "mhtml" {
continue;
}
let url_string = furl.to_string();
if seen.insert(url_string.clone()) {
let fmt_type = if ext_val == "mp4" || ext_val == "webm" || ext_val == "mkv" {
MediaType::Video
} else if ext_val == "mp3" || ext_val == "m4a" {
MediaType::Audio
} else {
MediaType::File
};
let q = format!("{}p", f.get("height").and_then(|v| v.as_u64()).unwrap_or(0));
result.media.push(MediaItem {
url: url_string,
quality: Some(q),
file_type: Some(fmt_type),
extension: Some(ext_val.to_string()),
thumbnail: data
.get("thumbnail")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
file_size: f
.get("filesize")
.and_then(|v| v.as_u64())
.map(format_filesize),
size_bytes: f.get("filesize").and_then(|v| v.as_u64()),
frame_width: f
.get("width")
.and_then(|v| v.as_u64())
.map(|w| w.to_string()),
frame_height: f
.get("height")
.and_then(|v| v.as_u64())
.map(|h| h.to_string()),
note: None,
});
}
}
}
}
// If no formats listed, fall back to the top-level url
if result.media.is_empty() {
if let Some(dl_url) = data.get("url").and_then(|v| v.as_str()) {
result.media.push(MediaItem {
url: dl_url.to_string(),
quality: Some(format!(
"{}p",
data.get("height").and_then(|v| v.as_u64()).unwrap_or(0)
)),
file_type: Some(MediaType::Video),
extension: Some(
data.get("ext")
.and_then(|v| v.as_str())
.unwrap_or("mp4")
.to_string(),
),
thumbnail: data
.get("thumbnail")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
file_size: None,
size_bytes: None,
frame_width: None,
frame_height: None,
note: None,
});
}
}
Ok(result)
}
// ============================================================================
// Reddit downloader (via yt-dlp)
// ============================================================================
pub async fn fetch_reddit(url: &str) -> Result<DownloadResult, ScrapingError> {
if !url.contains("reddit.com") && !url.contains("redd.it") {
return Err(ScrapingError::Http("Invalid Reddit URL".to_string()));
}
let data = run_ytdlp_json(url, &["-f", "bestvideo+bestaudio/best"]).await?;
let title = data
.get("title")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let author = data
.get("uploader")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let thumbnail = data
.get("thumbnail")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let duration = data
.get("duration")
.and_then(|v| v.as_u64())
.map(|d| format!("{}s", d));
let mut result = DownloadResult::success(title);
result.author = author;
result.thumbnail = thumbnail;
result.duration = duration;
result.provider = Some("yt-dlp".to_string());
let formats = data.get("formats").and_then(|v| v.as_array());
let mut seen = std::collections::HashSet::new();
if let Some(fmts) = formats {
for f in fmts {
if let (Some(furl), Some(ext_val)) = (
f.get("url").and_then(|v| v.as_str()),
f.get("ext").and_then(|v| v.as_str()),
) {
if ext_val == "mhtml" {
continue;
}
let url_string = furl.to_string();
if seen.insert(url_string.clone()) {
let fmt_type = if ext_val == "mp4" || ext_val == "webm" || ext_val == "mkv" {
MediaType::Video
} else if ext_val == "mp3" || ext_val == "m4a" {
MediaType::Audio
} else {
MediaType::File
};
let q = format!("{}p", f.get("height").and_then(|v| v.as_u64()).unwrap_or(0));
result.media.push(MediaItem {
url: url_string,
quality: Some(q),
file_type: Some(fmt_type),
extension: Some(ext_val.to_string()),
thumbnail: data
.get("thumbnail")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
file_size: f
.get("filesize")
.and_then(|v| v.as_u64())
.map(format_filesize),
size_bytes: f.get("filesize").and_then(|v| v.as_u64()),
frame_width: f
.get("width")
.and_then(|v| v.as_u64())
.map(|w| w.to_string()),
frame_height: f
.get("height")
.and_then(|v| v.as_u64())
.map(|h| h.to_string()),
note: None,
});
}
}
}
}
// If no formats listed, fall back to the top-level url
if result.media.is_empty() {
if let Some(dl_url) = data.get("url").and_then(|v| v.as_str()) {
result.media.push(MediaItem {
url: dl_url.to_string(),
quality: Some(format!(
"{}p",
data.get("height").and_then(|v| v.as_u64()).unwrap_or(0)
)),
file_type: Some(MediaType::Video),
extension: Some(
data.get("ext")
.and_then(|v| v.as_str())
.unwrap_or("mp4")
.to_string(),
),
thumbnail: data
.get("thumbnail")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
file_size: None,
size_bytes: None,
frame_width: None,
frame_height: None,
note: None,
});
}
}
Ok(result)
}
// ============================================================================
// Pinterest downloader
// ============================================================================
+16
View File
@@ -195,6 +195,22 @@ pub async fn download_soundcloud(
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _dailymotion.
pub async fn download_dailymotion(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_dailymotion(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _reddit.
pub async fn download_reddit(
Query(params): Query<DownloadParams>,
) -> Result<Json<DownloadResponse>, AppError> {
let result = use_cases::download_reddit(&params.url).await?;
Ok(Json(DownloadResponse::ok(result)))
}
/// Handler for _bilibili.
pub async fn download_bilibili(
Query(params): Query<DownloadParams>,
+8
View File
@@ -236,6 +236,14 @@ pub fn build_router(app_state: Arc<AppState>) -> anyhow::Result<Router> {
"/download/soundcloud",
axum::routing::get(crate::presentation::handler::downloader::download_soundcloud),
)
.route(
"/download/dailymotion",
axum::routing::get(crate::presentation::handler::downloader::download_dailymotion),
)
.route(
"/download/reddit",
axum::routing::get(crate::presentation::handler::downloader::download_reddit),
)
.route(
"/download/bilibili",
axum::routing::get(crate::presentation::handler::downloader::download_bilibili),