diff --git a/src/providers/mod.rs b/src/providers/mod.rs index f10f600..f029c35 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -35,6 +35,7 @@ pub mod beeg; pub mod tnaflix; pub mod pornxp; pub mod rule34gen; +pub mod xxdbx; // convenient alias pub type DynProvider = Arc; @@ -46,6 +47,7 @@ pub static ALL_PROVIDERS: Lazy> = Lazy::new(| m.insert("tnaflix", Arc::new(tnaflix::TnaflixProvider::new()) as DynProvider); m.insert("pornxp", Arc::new(pornxp::PornxpProvider::new()) as DynProvider); m.insert("rule34gen", Arc::new(rule34gen::Rule34genProvider::new()) as DynProvider); + m.insert("xxdbx", Arc::new(xxdbx::XxdbxProvider::new()) as DynProvider); // add more here as you migrate them m }); diff --git a/src/providers/xxdbx.rs b/src/providers/xxdbx.rs new file mode 100644 index 0000000..7b3ca6c --- /dev/null +++ b/src/providers/xxdbx.rs @@ -0,0 +1,277 @@ +use crate::DbPool; +use crate::api::ClientVersion; +use crate::providers::Provider; +use crate::status::*; +use crate::util::cache::VideoCache; +use crate::util::time::parse_time_to_seconds; +use crate::videos::{ServerOptions, VideoItem}; +use async_trait::async_trait; +use error_chain::error_chain; +use htmlentity::entity::{ICodedDataTrait, decode}; +use std::sync::{Arc, RwLock}; +use std::vec; + +error_chain! { + foreign_links { + Io(std::io::Error); + HttpRequest(wreq::Error); + } +} + +#[derive(Debug, Clone)] +pub struct XxdbxProvider { + url: String, + stars: Arc>>, + channels: Arc>>, +} +impl XxdbxProvider { + pub fn new() -> Self { + let provider = XxdbxProvider { + url: "https://xxdbx.com".to_string(), + stars: Arc::new(RwLock::new(vec![])), + channels: Arc::new(RwLock::new(vec![])), + }; + provider + } + + fn build_channel(&self, clientversion: ClientVersion) -> Channel { + let _ = clientversion; + Channel { + id: "xxdbx".to_string(), + name: "xxdbx".to_string(), + description: "XXX Video Database".to_string(), + premium: false, + favicon: "https://www.google.com/s2/favicons?sz=64&domain=xxdbx.com".to_string(), + status: "active".to_string(), + categories: vec![], + options: vec![ + ChannelOption { + id: "sort".to_string(), + title: "Sort".to_string(), + description: "Sort the Videos".to_string(), + systemImage: "list.number".to_string(), + colorName: "blue".to_string(), + options: vec![ + FilterOption { + id: "new".into(), + title: "New".into(), + }, + FilterOption { + id: "popular".into(), + title: "Most Popular".into(), + }, + ], + multiSelect: false, + }, + ], + nsfw: true, + cacheDuration: None, + } + } + + async fn get( + &self, + cache: VideoCache, + page: u8, + sort: &str, + options: ServerOptions, + ) -> Result> { + let sort_string: String = match sort { + "popular" => "most-popular".to_string(), + _ => "".to_string(), + }; + let video_url = format!( + "{}/{}?page={}", + self.url, sort_string, page + ); + let old_items = match cache.get(&video_url) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { + println!("Cache hit for URL: {}", video_url); + return Ok(items.clone()); + } else { + items.clone() + } + } + None => { + vec![] + } + }; + + let mut requester = options.requester.clone().unwrap(); + let text = requester.get(&video_url).await.unwrap(); + let video_items: Vec = self.get_video_items_from_html(text.clone()); + 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 query( + &self, + cache: VideoCache, + page: u8, + query: &str, + options: ServerOptions, + ) -> Result> { + let search_string = query.trim().to_string(); + let mut search_type = "search"; + + if self.channels.read().unwrap().contains(&search_string) { + search_type = "channels"; + } else if self.stars.read().unwrap().contains(&search_string) { + search_type = "stars"; + } + + let video_url = format!( + "{}/{}/{}?page={}", + self.url, search_type, search_string, page + ); + // 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 * 5 { + return Ok(items.clone()); + } else { + let _ = cache.check().await; + return Ok(items.clone()); + } + } + None => { + vec![] + } + }; + + let mut requester = options.requester.clone().unwrap(); + + let text = requester.get(&video_url).await.unwrap(); + let video_items: Vec = self.get_video_items_from_html(text.clone()); + 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) + } + + fn get_video_items_from_html(&self, html: String) -> Vec { + if html.is_empty() { + println!("HTML is empty"); + return vec![]; + } + let mut items: Vec = Vec::new(); + let raw_videos = html.split("").collect::>()[0] + .split("
") + .collect::>()[1] + .split("
") + .collect::>()[1..] + .to_vec(); + for video_segment in &raw_videos { + let vid = video_segment.split("\n").collect::>(); + for (index, line) in vid.iter().enumerate() { + println!("Line {}: {}\n\n", index, line); + } + let video_url: String = format!("{}{}", self.url, video_segment.split(">()[1] + .split("\"") + .collect::>()[0] + .to_string()); + let mut title = video_segment + .split("
") + .collect::>()[1] + .split("<") + .collect::>()[0] + .trim() + .to_string(); + // html decode + title = decode(title.as_bytes()).to_string().unwrap_or(title); + let id = video_url.split("/").collect::>()[4].to_string(); + + let thumb = format!("https:{}", video_segment.split(">()[1] + .split("src=\"").collect::>().last().unwrap() + .split("\"") + .collect::>()[0] + .to_string()); + let raw_duration = video_segment + .split("
") + .collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32; + + let preview = format!("https:{}",video_segment + .split("data-preview=\"") + .collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string()); + let tags = video_segment.split("
").collect::>()[1] + .split("
").collect::>()[0] + .split("
>()[1..] + .into_iter().map(|s| s.split("\"").collect::>()[0].replace("%20"," ").to_string()).collect::>(); + for tag in tags.clone() { + let shorted_tag = tag.split("/").collect::>()[2].to_string(); + if tag.contains("channels") && self.channels.read().unwrap().contains(&shorted_tag) == false { + self.channels.write().unwrap().push(shorted_tag.clone()); + } + if tag.contains("stars") && self.stars.read().unwrap().contains(&shorted_tag) == false { + self.stars.write().unwrap().push(shorted_tag.clone()); + } + } + let video_item = VideoItem::new( + id, + title, + video_url.to_string(), + "xxdbx".to_string(), + thumb, + duration, + ) + .tags(tags) + .preview(preview); + items.push(video_item); + } + return items; + } +} + +#[async_trait] +impl Provider for XxdbxProvider { + async fn get_videos( + &self, + cache: VideoCache, + pool: DbPool, + sort: String, + query: Option, + page: String, + per_page: String, + options: ServerOptions, + ) -> Vec { + let _ = per_page; + let _ = pool; + let videos: std::result::Result, Error> = match query { + Some(q) => { + self.query(cache, page.parse::().unwrap_or(1), &q, options) + .await + } + None => { + self.get(cache, page.parse::().unwrap_or(1), &sort, options) + .await + } + }; + match videos { + Ok(v) => v, + Err(e) => { + println!("Error fetching videos: {}", e); + vec![] + } + } + } + fn get_channel(&self, clientversion: ClientVersion) -> crate::status::Channel { + self.build_channel(clientversion) + } +}