video from url
This commit is contained in:
267
src/api.rs
267
src/api.rs
@@ -12,9 +12,12 @@ use crate::{DbPool, db, status::*, videos::*};
|
||||
use ntex::http::header;
|
||||
use ntex::web;
|
||||
use ntex::web::HttpRequest;
|
||||
use serde_json::Value;
|
||||
use std::cmp::Ordering;
|
||||
use std::io;
|
||||
use std::process::Command;
|
||||
use tokio::task;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientVersion {
|
||||
@@ -132,6 +135,154 @@ fn video_matches_literal_query(video: &VideoItem, literal_query: &str) -> bool {
|
||||
.is_some_and(|tags| tags.iter().any(|tag| contains_literal(tag)))
|
||||
}
|
||||
|
||||
fn normalize_query_url(query: &str) -> Option<String> {
|
||||
let trimmed = query.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = Url::parse(trimmed).ok()?;
|
||||
match parsed.scheme() {
|
||||
"http" | "https" => Some(parsed.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn video_item_from_ytdlp_payload(
|
||||
channel: &str,
|
||||
fallback_url: &str,
|
||||
payload: &Value,
|
||||
) -> Option<VideoItem> {
|
||||
let title = payload
|
||||
.get("title")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.trim().is_empty())?
|
||||
.to_string();
|
||||
let page_url = payload
|
||||
.get("webpage_url")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| value.starts_with("http://") || value.starts_with("https://"))
|
||||
.unwrap_or(fallback_url)
|
||||
.to_string();
|
||||
let id = payload
|
||||
.get("id")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
Url::parse(&page_url)
|
||||
.ok()
|
||||
.and_then(|parsed| parsed.path_segments()?.next_back().map(ToOwned::to_owned))
|
||||
})?;
|
||||
let thumb = payload
|
||||
.get("thumbnail")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let duration = payload
|
||||
.get("duration")
|
||||
.and_then(|value| value.as_u64())
|
||||
.and_then(|value| u32::try_from(value).ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut item = VideoItem::new(id, title, page_url, channel.to_string(), thumb, duration);
|
||||
item.views = payload
|
||||
.get("view_count")
|
||||
.and_then(|value| value.as_u64())
|
||||
.and_then(|value| u32::try_from(value).ok());
|
||||
item.uploader = payload
|
||||
.get("uploader")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
item.uploaderUrl = payload
|
||||
.get("uploader_url")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
item.preview = payload
|
||||
.get("thumbnail")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
let formats = payload
|
||||
.get("formats")
|
||||
.and_then(|value| value.as_array())
|
||||
.map(|entries| {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|format| {
|
||||
let format_url =
|
||||
format
|
||||
.get("url")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| {
|
||||
value.starts_with("http://") || value.starts_with("https://")
|
||||
})?;
|
||||
let quality = format
|
||||
.get("format_id")
|
||||
.and_then(|value| value.as_str())
|
||||
.or_else(|| format.get("format").and_then(|value| value.as_str()))
|
||||
.or_else(|| format.get("resolution").and_then(|value| value.as_str()))
|
||||
.unwrap_or("auto")
|
||||
.to_string();
|
||||
let ext = format
|
||||
.get("ext")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("mp4")
|
||||
.to_string();
|
||||
|
||||
let mut video_format =
|
||||
VideoFormat::new(format_url.to_string(), quality.clone(), ext)
|
||||
.format_id(quality.clone());
|
||||
if let Some(note) = format.get("format_note").and_then(|value| value.as_str()) {
|
||||
if !note.trim().is_empty() {
|
||||
video_format = video_format.format_note(note.to_string());
|
||||
}
|
||||
}
|
||||
Some(video_format)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if !formats.is_empty() {
|
||||
item.formats = Some(formats);
|
||||
}
|
||||
|
||||
Some(item)
|
||||
}
|
||||
|
||||
fn videos_from_ytdlp_query_url(
|
||||
channel: &str,
|
||||
query_url: &str,
|
||||
limit: usize,
|
||||
) -> Option<Vec<VideoItem>> {
|
||||
let output = Command::new("yt-dlp")
|
||||
.arg("-J")
|
||||
.arg("--no-warnings")
|
||||
.arg("--extractor-args")
|
||||
.arg("generic:impersonate=chrome")
|
||||
.arg(query_url)
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let payload: Value = serde_json::from_slice(&output.stdout).ok()?;
|
||||
if let Some(entries) = payload.get("entries").and_then(|value| value.as_array()) {
|
||||
let items = entries
|
||||
.iter()
|
||||
.filter_map(|entry| video_item_from_ytdlp_payload(channel, query_url, entry))
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
return (!items.is_empty()).then_some(items);
|
||||
}
|
||||
|
||||
video_item_from_ytdlp_payload(channel, query_url, &payload).map(|item| vec![item])
|
||||
}
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("/status")
|
||||
@@ -418,6 +569,65 @@ async fn videos_post(
|
||||
sort: Some(sort.clone()),
|
||||
sexuality: Some(sexuality),
|
||||
};
|
||||
|
||||
if let Some(query_url) = query.as_deref().and_then(normalize_query_url) {
|
||||
crate::flow_debug!(
|
||||
"trace={} videos attempting ytdlp url fast path provider={} url={}",
|
||||
trace_id,
|
||||
&channel,
|
||||
crate::util::flow_debug::preview(&query_url, 160)
|
||||
);
|
||||
if let Some(mut video_items) =
|
||||
videos_from_ytdlp_query_url(&channel, &query_url, perPage as usize)
|
||||
{
|
||||
if clientversion == ClientVersion::new(38, 0, "Hot%20Tub".to_string()) {
|
||||
video_items = video_items
|
||||
.into_iter()
|
||||
.filter_map(|video| {
|
||||
let last_url = video
|
||||
.formats
|
||||
.as_ref()
|
||||
.and_then(|formats| formats.last().map(|f| f.url.clone()));
|
||||
if let Some(url) = last_url {
|
||||
let mut v = video;
|
||||
v.url = url;
|
||||
return Some(v);
|
||||
}
|
||||
Some(video)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
for video in video_items.iter_mut() {
|
||||
if video.duration <= 120 {
|
||||
let mut preview_url = video.url.clone();
|
||||
if let Some(formats) = &video.formats {
|
||||
if let Some(first) = formats.first() {
|
||||
preview_url = first.url.clone();
|
||||
}
|
||||
}
|
||||
video.preview = Some(preview_url);
|
||||
}
|
||||
}
|
||||
|
||||
videos.pageInfo = PageInfo {
|
||||
hasNextPage: false,
|
||||
resultsPerPage: perPage as u32,
|
||||
};
|
||||
videos.items = video_items;
|
||||
crate::flow_debug!(
|
||||
"trace={} videos ytdlp url fast path returned count={}",
|
||||
trace_id,
|
||||
videos.items.len()
|
||||
);
|
||||
return Ok(web::HttpResponse::Ok().json(&videos));
|
||||
}
|
||||
crate::flow_debug!(
|
||||
"trace={} videos ytdlp url fast path fell back to provider",
|
||||
trace_id
|
||||
);
|
||||
}
|
||||
|
||||
crate::flow_debug!(
|
||||
"trace={} videos provider dispatch provider={} literal_query={:?}",
|
||||
trace_id,
|
||||
@@ -774,4 +984,61 @@ mod tests {
|
||||
|
||||
assert!(uploader_match_sort_key(&b) > uploader_match_sort_key(&a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_http_and_https_query_urls() {
|
||||
assert_eq!(
|
||||
normalize_query_url(" https://www.freeuseporn.com/video/9579/example "),
|
||||
Some("https://www.freeuseporn.com/video/9579/example".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query_url("http://example.com/video"),
|
||||
Some("http://example.com/video".to_string())
|
||||
);
|
||||
assert_eq!(normalize_query_url("Nicole Kitt"), None);
|
||||
assert_eq!(normalize_query_url("ftp://example.com/video"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_video_item_from_ytdlp_payload() {
|
||||
let payload = serde_json::json!({
|
||||
"id": "9579",
|
||||
"title": "Nicole Kitt - Example",
|
||||
"webpage_url": "https://www.freeuseporn.com/video/9579/nicole-kitt-example",
|
||||
"thumbnail": "https://www.freeuseporn.com/media/videos/tmb/9579/1.jpg",
|
||||
"duration": 3549,
|
||||
"view_count": 52180,
|
||||
"uploader": "FreeusePorn",
|
||||
"formats": [
|
||||
{
|
||||
"url": "https://www.freeuseporn.com/media/videos/h264/9579_720p.mp4",
|
||||
"format_id": "720p",
|
||||
"format_note": "720p",
|
||||
"ext": "mp4"
|
||||
},
|
||||
{
|
||||
"url": "https://www.freeuseporn.com/media/videos/h264/9579_480p.mp4",
|
||||
"format_id": "480p",
|
||||
"ext": "mp4"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let item = video_item_from_ytdlp_payload(
|
||||
"freeuseporn",
|
||||
"https://www.freeuseporn.com/video/9579/nicole-kitt-example",
|
||||
&payload,
|
||||
)
|
||||
.expect("item should parse");
|
||||
|
||||
assert_eq!(item.id, "9579");
|
||||
assert_eq!(item.title, "Nicole Kitt - Example");
|
||||
assert_eq!(
|
||||
item.url,
|
||||
"https://www.freeuseporn.com/video/9579/nicole-kitt-example"
|
||||
);
|
||||
assert_eq!(item.views, Some(52180));
|
||||
assert_eq!(item.uploader.as_deref(), Some("FreeusePorn"));
|
||||
assert_eq!(item.formats.as_ref().map(|formats| formats.len()), Some(2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,13 +313,13 @@ impl FreeusepornProvider {
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
.views(views.unwrap_or(0))
|
||||
.formats(self.build_formats(&id));
|
||||
.views(views.unwrap_or(0));
|
||||
|
||||
if views.is_none() {
|
||||
item.views = None;
|
||||
}
|
||||
item.rating = rating;
|
||||
item.formats = Some(self.build_formats(&id));
|
||||
|
||||
Some(item)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user