Files
hottub/src/providers/xfree.rs
2026-03-08 22:26:35 +00:00

320 lines
10 KiB
Rust

use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::Provider;
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::requester::Requester;
use crate::videos::{ServerOptions, VideoItem};
use async_trait::async_trait;
use error_chain::error_chain;
use std::sync::{Arc, RwLock};
use std::vec;
use wreq::Version;
error_chain! {
foreign_links {
Io(std::io::Error);
HttpRequest(wreq::Error);
Json(serde_json::Error);
}
errors {
Parse(msg: String) {
description("parse error")
display("parse error: {}", msg)
}
}
}
#[derive(Debug, Clone)]
pub struct XfreeProvider {
url: String,
categories: Arc<RwLock<Vec<FilterOption>>>,
}
impl XfreeProvider {
pub fn new() -> Self {
let provider = Self {
url: "https://www.xfree.com".to_string(),
categories: Arc::new(RwLock::new(vec![])),
};
provider
}
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
let _ = clientversion;
Channel {
id: "xfree".to_string(),
name: "XFree".to_string(),
description: "Reels & Nudes!".to_string(),
premium: false,
favicon: "https://www.google.com/s2/favicons?sz=64&domain=xfree.com".to_string(),
status: "active".to_string(),
categories: self
.categories
.read()
.map(|categories| categories.iter().map(|c| c.title.clone()).collect())
.unwrap_or_else(|e| {
crate::providers::report_provider_error_background(
"xfree",
"build_channel.categories_read",
&e.to_string(),
);
vec![]
}),
options: vec![
ChannelOption {
id: "sexuality".to_string(),
title: "Sexuality".to_string(),
description: "Sexuality of the Videos".to_string(),
systemImage: "heart".to_string(),
colorName: "red".to_string(),
multiSelect: false,
options: vec![
FilterOption {
id: "1".to_string(),
title: "Straight".to_string(),
},
FilterOption {
id: "2".to_string(),
title: "Gay".to_string(),
},
FilterOption {
id: "3".to_string(),
title: "Trans".to_string(),
},
],
},
],
nsfw: true,
cacheDuration: None,
}
}
fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
if let Ok(mut vec) = target.write() {
if !vec.iter().any(|x| x.id == item.id) {
vec.push(item);
}
}
}
async fn query(
&self,
cache: VideoCache,
page: u8,
query: &str,
options: ServerOptions,
pool: DbPool,
) -> Result<Vec<VideoItem>> {
let query = if query.is_empty() { "null" } else { query };
let sexuality = match options.clone().sexuality {
Some(s) if !s.is_empty() => s,
_ => "1".to_string(),
};
let video_url = format!(
"{}/api/2/search?search={}&lgbt={}&limit=30&offset={}",
self.url,
query.replace(" ", "%20"),
sexuality,
(page as u32 - 1) * 30
);
// Check our Video Cache. If the result is younger than 1 hour, we return it.
let old_items = match cache.get(&video_url) {
Some((time, items)) => {
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 * 24 {
return Ok(items.clone());
} else {
let _ = cache.check().await;
return Ok(items.clone());
}
}
None => {
vec![]
}
};
let mut requester =
crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
// let _ = requester.get("https://www.xfree.com/", Some(Version::HTTP_2)).await;
let text = match requester.get_with_headers(&video_url, vec![
("Apiversion".to_string(), "1.0".to_string()),
("Accept".to_string(), "application/json text/plain */*".to_string()),
("Referer".to_string(), "https://www.xfree.com/".to_string()),
],
Some(Version::HTTP_2),
).await {
Ok(text) => text,
Err(e) => {
crate::providers::report_provider_error(
"xfree",
"query.request",
&format!("url={video_url}; error={e}"),
)
.await;
return Ok(old_items);
}
};
let video_items: Vec<VideoItem> = self
.get_video_items_from_json(text.clone(), &mut requester, pool)
.await;
if !video_items.is_empty() {
cache.remove(&video_url);
cache.insert(video_url.clone(), video_items.clone());
} else {
return Ok(old_items);
}
Ok(video_items)
}
async fn get_video_items_from_json(
&self,
html: String,
_requester: &mut Requester,
_pool: DbPool,
) -> Vec<VideoItem> {
let mut items: Vec<VideoItem> = Vec::new();
let json_result = serde_json::from_str::<serde_json::Value>(&html);
let json = match json_result {
Ok(json) => json,
Err(e) => {
eprintln!("Failed to parse JSON: {e}");
crate::providers::report_provider_error(
"xfree",
"get_video_items_from_json.parse",
&format!("Failed to parse JSON: {e}"),
)
.await;
return vec![];
}
};
for post in json.get("body").and_then(|v| v.get("posts"))
.and_then(|p| p.as_array())
.unwrap_or(&vec![])
{
let id = post
.get("media")
.and_then(|v| v.get("name"))
.and_then(|v| v.as_str())
.unwrap_or_default();
let title = post
.get("title")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let video_url = format!(
"https://cdn.xfree.com/xfree-prod/{}/{}/{}/{}/full.mp4",
id.chars().nth(0).unwrap_or('0'),
id.chars().nth(1).unwrap_or('0'),
id.chars().nth(2).unwrap_or('0'),
id
);
let listsuffix = post
.get("media")
.and_then(|v| v.get("listingSuffix"))
.and_then(|v| v.as_i64())
.unwrap_or_default();
let thumb = format!(
"https://thumbs.xfree.com/listing/medium/{}_{}.webp",
id, listsuffix
);
let views = post.get("viewCount").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let preview = format!(
"https://cdn.xfree.com/xfree-prod/{}/{}/{}/{}/listing7.mp4",
id.chars().nth(0).unwrap_or('0'),
id.chars().nth(1).unwrap_or('0'),
id.chars().nth(2).unwrap_or('0'),
id
);
let duration = post
.get("media")
.and_then(|v| v.get("duration"))
.and_then(|v| v.as_f64())
.unwrap_or_default() as u32;
let tags = post
.get("tags")
.and_then(|v| v.as_array())
.unwrap_or(&vec![])
.iter()
.filter_map(|t|
t.get("tag").and_then(|n| n.as_str()).map(|s| s.to_string()))
.collect::<Vec<String>>();
for tag in tags.iter() {
Self::push_unique(&self.categories, FilterOption {
id: tag.clone(),
title: tag.clone(),
});
}
let uploader = post
.get("user")
.and_then(|v| v.get("displayName"))
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let upload_date = post
.get("publishedDate")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let uploaded_at = chrono::DateTime::parse_from_rfc3339(&upload_date)
.map(|dt| dt.timestamp() as u64)
.unwrap_or(0);
let aspect_ration = post
.get("media")
.and_then(|v| v.get("aspectRatio"))
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string()
.parse::<f32>()
.unwrap_or(0.5625)
;
let video_item = VideoItem::new(
id.to_string(),
title,
video_url,
"xfree".to_string(),
thumb,
duration,
)
.views(views)
.preview(preview)
.tags(tags)
.uploader(uploader)
.uploaded_at(uploaded_at)
.aspect_ratio(aspect_ration);
items.push(video_item);
}
return items;
}
}
#[async_trait]
impl Provider for XfreeProvider {
async fn get_videos(
&self,
cache: VideoCache,
pool: DbPool,
_sort: String,
query: Option<String>,
page: String,
_per_page: String,
options: ServerOptions,
) -> Vec<VideoItem> {
let page = page.parse::<u8>().unwrap_or(1);
let res = self.to_owned().query(cache, page, &query.unwrap_or("null".to_string()), options, pool).await;
res.unwrap_or_else(|e| {
eprintln!("xfree error: {e}");
vec![]
})
}
fn get_channel(&self, v: ClientVersion) -> Option<Channel> {
Some(self.build_channel(v))
}
}