use crate::DbPool; use crate::api::ClientVersion; use crate::providers::Provider; use crate::util::cache::VideoCache; use crate::util::parse_abbreviated_number; use crate::util::time::parse_time_to_seconds; use crate::videos::{ServerOptions, VideoItem}; use crate::{status::*, util}; use async_trait::async_trait; use error_chain::error_chain; use htmlentity::entity::{ICodedDataTrait, decode}; use serde_json::Value; use std::sync::{Arc, RwLock}; use std::thread; use std::vec; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); } } #[derive(Debug, Clone)] pub struct BeegProvider { sites: Arc>>, stars: Arc>>, categories: Arc>>, } impl BeegProvider { pub fn new() -> Self { let provider = BeegProvider { sites: Arc::new(RwLock::new(vec![FilterOption { id: "all".to_string(), title: "All".to_string(), }])), stars: Arc::new(RwLock::new(vec![FilterOption { id: "all".to_string(), title: "All".to_string(), }])), categories: Arc::new(RwLock::new(vec![FilterOption { id: "all".to_string(), title: "All".to_string(), }])), }; // Kick off the background load but return immediately provider.spawn_initial_load(); provider } fn spawn_initial_load(&self) { let sites = Arc::clone(&self.sites); let categories = Arc::clone(&self.categories); let stars = Arc::clone(&self.stars); thread::spawn(move || { // Create a tiny runtime just for these async tasks let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("build tokio runtime"); rt.block_on(async move { // If you have a streaming sites loader, call it here too if let Err(e) = Self::load_sites(sites).await { eprintln!("beeg load_sites_into failed: {e}"); } if let Err(e) = Self::load_categories(categories).await { eprintln!("beeg load_categories failed: {e}"); } if let Err(e) = Self::load_stars(stars).await { eprintln!("beeg load_stars failed: {e}"); } }); }); } async fn load_stars(stars: Arc>>) -> Result<()> { let mut requester = util::requester::Requester::new(); let text = requester .get("https://store.externulls.com/tag/facts/tags?get_original=true&slug=index", None) .await .unwrap(); let json: serde_json::Value = serde_json::from_str::(&text).unwrap(); let stars_array = json.get("human").unwrap().as_array().unwrap(); for s in stars_array { let star_name = s.get("tg_name").unwrap().as_str().unwrap().to_string(); let star_id = s.get("tg_slug").unwrap().as_str().unwrap().to_string(); Self::push_unique( &stars, FilterOption { id: star_id, title: star_name, }, ); } return Ok(()); } async fn load_categories(categories: Arc>>) -> Result<()> { let mut requester = util::requester::Requester::new(); let text = requester .get("https://store.externulls.com/tag/facts/tags?get_original=true&slug=index", None) .await .unwrap(); let json: serde_json::Value = serde_json::from_str::(&text).unwrap(); let stars_array = json.get("other").unwrap().as_array().unwrap(); for s in stars_array { let star_name = s.get("tg_name").unwrap().as_str().unwrap().to_string(); let star_id = s.get("tg_slug").unwrap().as_str().unwrap().to_string(); Self::push_unique( &categories, FilterOption { id: star_id.replace("{","").replace("}",""), title: star_name.replace("{","").replace("}",""), }, ); } return Ok(()); } async fn load_sites(sites: Arc>>) -> Result<()> { let mut requester = util::requester::Requester::new(); let text = requester .get("https://store.externulls.com/tag/facts/tags?get_original=true&slug=index", None) .await .unwrap(); let json: serde_json::Value = serde_json::from_str::(&text).unwrap(); let stars_array = json.get("productions").unwrap().as_array().unwrap(); for s in stars_array { let star_name = s.get("tg_name").unwrap().as_str().unwrap().to_string(); let star_id = s.get("tg_slug").unwrap().as_str().unwrap().to_string(); Self::push_unique( &sites, FilterOption { id: star_id, title: star_name, }, ); } return Ok(()); } // Push one item with minimal lock time and dedup by id 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); // Optional: keep it sorted for nicer UX // vec.sort_by(|a,b| a.title.cmp(&b.title)); } } } fn build_channel(&self, clientversion: ClientVersion) -> Channel { let _ = clientversion; let sites: Vec = self .sites .read() .map(|g| g.clone()) // or: .map(|g| g.to_vec()) .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new()) let categories: Vec = self .categories .read() .map(|g| g.clone()) // or: .map(|g| g.to_vec()) .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new()) let stars: Vec = self .stars .read() .map(|g| g.clone()) // or: .map(|g| g.to_vec()) .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new()) Channel { id: "beeg".to_string(), name: "Beeg".to_string(), description: "Watch your favorite Porn on Beeg.com".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=beeg.com".to_string(), status: "active".to_string(), categories: vec![], options: vec![ ChannelOption { id: "sites".to_string(), title: "Sites".to_string(), description: "Filter for different Sites".to_string(), systemImage: "rectangle.stack".to_string(), colorName: "green".to_string(), options: sites, multiSelect: false, }, ChannelOption { id: "categories".to_string(), title: "Categories".to_string(), description: "Filter for different Networks".to_string(), systemImage: "list.dash".to_string(), colorName: "purple".to_string(), options: categories, multiSelect: false, }, ChannelOption { id: "stars".to_string(), title: "Stars".to_string(), description: "Filter for different Pornstars".to_string(), systemImage: "star.fill".to_string(), colorName: "yellow".to_string(), options: stars, multiSelect: false, }, ], nsfw: true, cacheDuration: None, } } async fn get( &self, cache: VideoCache, page: u8, options: ServerOptions, ) -> Result> { let mut slug = ""; if options.categories.is_some() && !options.categories.as_ref().unwrap().is_empty() && options.categories.as_ref().unwrap() != "all" { slug = options.categories.as_ref().unwrap(); } if options.sites.is_some() && !options.sites.as_ref().unwrap().is_empty() && options.sites.as_ref().unwrap() != "all" { slug = options.sites.as_ref().unwrap(); } if options.stars.is_some() && !options.stars.as_ref().unwrap().is_empty() && options.stars.as_ref().unwrap() != "all" { slug = options.stars.as_ref().unwrap(); } let video_url = format!( "https://store.externulls.com/facts/tag?limit=100&offset={}{}", page - 1, match slug { "" => "&id=27173".to_string(), _ => format!("&slug={}", slug.replace(" ", "")), } ); 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, None).await.unwrap(); let json: serde_json::Value = serde_json::from_str::(&text).unwrap(); let video_items: Vec = self.get_video_items_from_html(json.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 video_url = format!( "https://store.externulls.com/facts/tag?get_original=true&limit=100&offset={}&slug={}", page - 1, query.replace(" ", ""), ); // 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, None).await.unwrap(); let json: serde_json::Value = serde_json::from_str::(&text).unwrap(); let video_items: Vec = self.get_video_items_from_html(json.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, json: Value) -> Vec { let mut items: Vec = Vec::new(); let video_items = match json.as_array(){ Some(array) => array, None => return items, }; for video in video_items { // println!("video: {}\n\n\n", serde_json::to_string_pretty(&video).unwrap()); let file = match video.get("file"){ Some(v) => v, None => continue, }; let hls_resources = match file.get("hls_resources"){ Some(v) => v, None => continue, }; let video_key = match hls_resources.get("fl_cdn_multi"){ Some(v) => v, None => continue, }; let video_url = format!( "https://video.externulls.com/{}", video_key.to_string().replace("\"","") ); let data = match file.get("data") { Some(v) => v, None => continue, }; let title = match data[0].get("cd_value") { Some(v) => decode(v.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or(v.to_string()), None => "".to_string(), }; let id = match file.get("id"){ Some(v) => v.as_i64().unwrap_or(0).to_string(), None => title.clone(), }; let fc_facts = match video.get("fc_facts") { Some(v) => v[0].clone(), None => continue, }; let duration = match file.get("fl_duration") { Some(v) => parse_time_to_seconds(v.as_str().unwrap_or("0")).unwrap_or(0), None => 0, }; let tags = match video.get("tags") { Some(v) => { // v should be an array of tag objects v.as_array() .map(|arr| { arr.iter() .map(|tag| { tag.get("tg_name") .and_then(|name| name.as_str()) .unwrap_or("") .to_string() }) .collect::>() }) .unwrap_or_default() } None => Vec::new(), }; let thumb = format!("https://thumbs.externulls.com/videos/{}/0.webp?size=480x270", id); let views = match fc_facts.get("fc_st_views") { Some(v) => parse_abbreviated_number(v.as_str().unwrap_or("0")).unwrap_or(0), None => 0, }; let mut video_item = VideoItem::new( id, title, video_url.to_string(), "beeg".to_string(), thumb, duration as u32, ); if views > 0 { video_item = video_item.views(views); } if !tags.is_empty() { video_item = video_item.tags(tags); } items.push(video_item); } return items; } } #[async_trait] impl Provider for BeegProvider { async fn get_videos( &self, cache: VideoCache, _pool: DbPool, _sort: String, query: Option, page: String, _per_page: String, options: ServerOptions, ) -> Vec { 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), 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) } }