320 lines
10 KiB
Rust
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))
|
|
}
|
|
}
|