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::http::header;
|
||||||
use ntex::web;
|
use ntex::web;
|
||||||
use ntex::web::HttpRequest;
|
use ntex::web::HttpRequest;
|
||||||
|
use serde_json::Value;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::process::Command;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClientVersion {
|
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)))
|
.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) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::resource("/status")
|
web::resource("/status")
|
||||||
@@ -418,6 +569,65 @@ async fn videos_post(
|
|||||||
sort: Some(sort.clone()),
|
sort: Some(sort.clone()),
|
||||||
sexuality: Some(sexuality),
|
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!(
|
crate::flow_debug!(
|
||||||
"trace={} videos provider dispatch provider={} literal_query={:?}",
|
"trace={} videos provider dispatch provider={} literal_query={:?}",
|
||||||
trace_id,
|
trace_id,
|
||||||
@@ -774,4 +984,61 @@ mod tests {
|
|||||||
|
|
||||||
assert!(uploader_match_sort_key(&b) > uploader_match_sort_key(&a));
|
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,
|
thumb,
|
||||||
duration,
|
duration,
|
||||||
)
|
)
|
||||||
.views(views.unwrap_or(0))
|
.views(views.unwrap_or(0));
|
||||||
.formats(self.build_formats(&id));
|
|
||||||
|
|
||||||
if views.is_none() {
|
if views.is_none() {
|
||||||
item.views = None;
|
item.views = None;
|
||||||
}
|
}
|
||||||
item.rating = rating;
|
item.rating = rating;
|
||||||
|
item.formats = Some(self.build_formats(&id));
|
||||||
|
|
||||||
Some(item)
|
Some(item)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user