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; pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata = crate::providers::ProviderChannelMetadata { group_id: "mainstream-tube", tags: &["tube", "mixed", "search"], }; 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>>, } 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>>, 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> { 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 = 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 { let mut items: Vec = Vec::new(); let json_result = serde_json::from_str::(&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::>(); 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::() .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, page: String, _per_page: String, options: ServerOptions, ) -> Vec { let page = page.parse::().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 { Some(self.build_channel(v)) } }