diff --git a/.gitignore b/.gitignore index 109483f..668483f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ Cargo.lock *.db migrations/.keep .mcp.json +*.mp4* \ No newline at end of file diff --git a/src/providers/javtiful.rs b/src/providers/javtiful.rs index c650c5d..86749b3 100644 --- a/src/providers/javtiful.rs +++ b/src/providers/javtiful.rs @@ -1,12 +1,12 @@ use crate::DbPool; use crate::api::ClientVersion; -use crate::providers::Provider; +use crate::providers::{Provider, build_proxy_url, strip_url_scheme}; use crate::status::*; use crate::util::cache::VideoCache; use crate::util::discord::{format_error_chain, send_discord_error_report}; use crate::util::requester::Requester; use crate::util::time::parse_time_to_seconds; -use crate::videos::{ServerOptions, VideoFormat, VideoItem}; +use crate::videos::{ServerOptions, VideoItem}; use async_trait::async_trait; use error_chain::error_chain; @@ -362,20 +362,13 @@ impl JavtifulProvider { .unwrap_or("") .to_string(); let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; - let (tags, mut formats, views) = self - .extract_media(&video_url, &mut requester, options) - .await?; + let (tags, views) = self.extract_media(&video_url, &mut requester).await?; if preview.len() == 0 { preview = format!("https://trailers.jav.si/preview/{id}.mp4"); } - if formats.is_empty() && !preview.is_empty() { - let mut format = VideoFormat::new(preview.clone(), "preview".to_string(), "video/mp4".to_string()); - format.add_http_header("Referer".to_string(), video_url.clone()); - formats.push(format); - } - let video_item = VideoItem::new(id, title, video_url, "javtiful".into(), thumb, duration) - .formats(formats) + let proxy_url = build_proxy_url(options, "javtiful", &strip_url_scheme(&video_url)); + let video_item = VideoItem::new(id, title, proxy_url, "javtiful".into(), thumb, duration) .tags(tags) .preview(preview) .views(views); @@ -386,8 +379,7 @@ impl JavtifulProvider { &self, url: &str, requester: &mut Requester, - options: &ServerOptions, - ) -> Result<(Vec, Vec, u32)> { + ) -> Result<(Vec, u32)> { let text = requester .get(url, Some(Version::HTTP_2)) .await @@ -432,56 +424,7 @@ impl JavtifulProvider { .and_then(|s| s.replace(".", "").parse::().ok()) .unwrap_or(0); - let quality = "1080p".to_string(); - let mut formats = Vec::new(); - let video_id = url - .split("/video/") - .nth(1) - .and_then(|value| value.split('/').next()) - .unwrap_or("") - .trim(); - let token = text - .split("data-csrf-token=\"") - .nth(1) - .and_then(|value| value.split('"').next()) - .unwrap_or("") - .trim(); - - if !video_id.is_empty() && !token.is_empty() { - let form = wreq::multipart::Form::new() - .text("video_id", video_id.to_string()) - .text("pid_c", "".to_string()) - .text("token", token.to_string()); - - if let Ok(response) = requester - .post_multipart( - "https://javtiful.com/ajax/get_cdn", - form, - vec![("Referer".to_string(), url.to_string())], - Some(Version::HTTP_11), - ) - .await - { - let payload = response.text().await.unwrap_or_default(); - if let Ok(json) = serde_json::from_str::(&payload) { - if let Some(cdn_url) = json.get("playlists").and_then(|value| value.as_str()) { - if !cdn_url.trim().is_empty() { - let mut format = VideoFormat::new( - cdn_url.to_string(), - quality.clone(), - "m3u8".into(), - ); - format.add_http_header("Referer".to_string(), url.to_string()); - formats.push(format); - } - } - } - } - } - - let _ = options; - - Ok((tags, formats, views)) + Ok((tags, views)) } } diff --git a/src/proxies/javtiful.rs b/src/proxies/javtiful.rs index 92fedbd..95c04e7 100644 --- a/src/proxies/javtiful.rs +++ b/src/proxies/javtiful.rs @@ -1,4 +1,6 @@ use ntex::web; +use serde_json::Value; +use url::Url; use wreq::Version; use crate::util::requester::Requester; @@ -11,59 +13,151 @@ impl JavtifulProxy { JavtifulProxy {} } + fn normalize_detail_request(endpoint: &str) -> Option<(String, String)> { + let endpoint = endpoint.trim().trim_start_matches('/'); + if endpoint.is_empty() { + return None; + } + + let detail_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + endpoint.to_string() + } else if endpoint.starts_with("javtiful.com/") || endpoint.starts_with("www.javtiful.com/") + { + format!("https://{endpoint}") + } else { + format!("https://javtiful.com/{endpoint}") + }; + + let detail_url = if detail_url.starts_with("http://") { + detail_url.replacen("http://", "https://", 1) + } else { + detail_url + }; + + if !Self::is_allowed_detail_url(&detail_url) { + return None; + } + + let video_id = Url::parse(&detail_url) + .ok() + .and_then(|url| { + let mut segments = url.path_segments()?; + if segments.next()? != "video" { + return None; + } + segments.next().map(ToOwned::to_owned) + }) + .filter(|value| value.chars().all(|c| c.is_ascii_digit()) && !value.is_empty())?; + + Some((detail_url, video_id)) + } + + fn is_allowed_detail_url(url: &str) -> bool { + let Some(parsed) = Url::parse(url).ok() else { + return false; + }; + if parsed.scheme() != "https" { + return false; + } + let Some(host) = parsed.host_str() else { + return false; + }; + (host == "javtiful.com" || host == "www.javtiful.com") + && parsed.path().starts_with("/video/") + } + + fn extract_token(html: &str) -> Option { + html.split("data-csrf-token=\"") + .nth(1) + .and_then(|value| value.split('"').next()) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + } + + fn extract_playlist_url(payload: &str) -> Option { + let json = serde_json::from_str::(payload).ok()?; + json.get("playlist") + .and_then(Value::as_str) + .or_else(|| json.get("playlists").and_then(Value::as_str)) + .map(str::trim) + .map(ToOwned::to_owned) + .filter(|value| value.starts_with("https://")) + } + pub async fn get_video_url( &self, url: String, requester: web::types::State, ) -> String { let mut requester = requester.get_ref().clone(); - let endpoint = url - .trim_start_matches('/') - .strip_prefix("https://") - .or_else(|| url.trim_start_matches('/').strip_prefix("http://")) - .unwrap_or(url.trim_start_matches('/')) - .trim_start_matches("www.javtiful.com/") - .trim_start_matches("javtiful.com/") - .trim_start_matches('/') - .to_string(); - let detail_url = format!("https://javtiful.com/{endpoint}"); - let text = requester.get(&detail_url, None).await.unwrap_or_default(); - if text.is_empty() { - return "".to_string(); - } - let video_id = endpoint.split('/').nth(1).unwrap_or("").to_string(); + let Some((detail_url, video_id)) = Self::normalize_detail_request(&url) else { + return String::new(); + }; - let token = text - .split("data-csrf-token=\"") - .nth(1) - .and_then(|s| s.split('"').next()) - .unwrap_or("") - .to_string(); + let html = requester.get(&detail_url, Some(Version::HTTP_11)).await; + let Ok(html) = html else { + return String::new(); + }; + if html.is_empty() { + return String::new(); + } + + let Some(token) = Self::extract_token(&html) else { + return String::new(); + }; let form = wreq::multipart::Form::new() - .text("video_id", video_id.clone()) + .text("video_id", video_id) .text("pid_c", "".to_string()) - .text("token", token.clone()); + .text("token", token); let resp = match requester .post_multipart( "https://javtiful.com/ajax/get_cdn", form, - vec![("Referer".to_string(), detail_url)], + vec![ + ("Referer".to_string(), detail_url), + ("Origin".to_string(), "https://javtiful.com".to_string()), + ("Accept".to_string(), "*/*".to_string()), + ], Some(Version::HTTP_11), ) .await { Ok(r) => r, - Err(_) => return "".to_string(), + Err(_) => return String::new(), }; - let text = resp.text().await.unwrap_or_default(); - let json: serde_json::Value = - serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); - let video_url = json - .get("playlists") - .map(|v| v.to_string().replace("\"", "")) - .unwrap_or_default(); - - return video_url; + let payload = resp.text().await.unwrap_or_default(); + Self::extract_playlist_url(&payload).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::JavtifulProxy; + + #[test] + fn normalizes_detail_request_with_full_url() { + let (url, video_id) = + JavtifulProxy::normalize_detail_request("https://javtiful.com/video/106796/fns-176") + .expect("detail request should parse"); + assert_eq!(url, "https://javtiful.com/video/106796/fns-176"); + assert_eq!(video_id, "106796"); + } + + #[test] + fn normalizes_detail_request_with_path_only() { + let (url, video_id) = JavtifulProxy::normalize_detail_request("video/1000/demo") + .expect("detail request should parse"); + assert_eq!(url, "https://javtiful.com/video/1000/demo"); + assert_eq!(video_id, "1000"); + } + + #[test] + fn extracts_playlist_from_payload() { + let payload = r#"{"status":"ok","playlist":"https://cdn.example/106796.mp4"}"#; + assert_eq!( + JavtifulProxy::extract_playlist_url(payload).as_deref(), + Some("https://cdn.example/106796.mp4") + ); } }