use crate::DbPool; use crate::api::ClientVersion; use crate::providers::{Provider, report_provider_error, report_provider_error_background}; use crate::util::cache::VideoCache; use crate::util::parse_abbreviated_number; 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::time::Duration; use std::vec; 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 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".into(), title: "All".into(), }])), stars: Arc::new(RwLock::new(vec![FilterOption { id: "all".into(), title: "All".into(), }])), categories: Arc::new(RwLock::new(vec![FilterOption { id: "all".into(), title: "All".into(), }])), }; 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 || { let rt = match tokio::runtime::Builder::new_current_thread() .enable_all() .build() { Ok(rt) => rt, Err(e) => { eprintln!("beeg runtime init failed: {}", e); return; } }; rt.block_on(async move { match Self::fetch_tags().await { Ok(json) => { Self::load_sites(&json, sites); Self::load_categories(&json, categories); Self::load_stars(&json, stars); } Err(e) => { report_provider_error("beeg", "init.fetch_tags", &e.to_string()).await; } } }); }); } async fn fetch_tags() -> Result { let mut requester = util::requester::Requester::new(); let endpoints = [ "https://store.externulls.com/tag/facts/tags?get_original=true&slug=index", "https://store.externulls.com/tag/facts/tags?slug=index", ]; let mut errors: Vec = vec![]; for endpoint in endpoints { for attempt in 1..=3 { match requester.get(endpoint, None).await { Ok(text) => match serde_json::from_str::(&text) { Ok(json) => return Ok(json), Err(e) => { errors .push(format!("endpoint={endpoint}; attempt={attempt}; parse={e}")); } }, Err(e) => { errors.push(format!( "endpoint={endpoint}; attempt={attempt}; request={e}" )); } } tokio::time::sleep(Duration::from_millis(250 * attempt as u64)).await; } } Err(ErrorKind::Parse(format!("failed to fetch tags; {}", errors.join(" | "))).into()) } fn load_stars(json: &Value, stars: Arc>>) { let arr = json .get("human") .and_then(|v| v.as_array().map(|v| v.as_slice())) .unwrap_or(&[]); for s in arr { if let (Some(name), Some(id)) = ( s.get("tg_name").and_then(|v| v.as_str()), s.get("tg_slug").and_then(|v| v.as_str()), ) { Self::push_unique( &stars, FilterOption { id: id.into(), title: name.into(), }, ); } } } fn load_categories(json: &Value, categories: Arc>>) { let arr = json .get("other") .and_then(|v| v.as_array().map(|v| v.as_slice())) .unwrap_or(&[]); for s in arr { if let (Some(name), Some(id)) = ( s.get("tg_name").and_then(|v| v.as_str()), s.get("tg_slug").and_then(|v| v.as_str()), ) { Self::push_unique( &categories, FilterOption { id: id.replace('{', "").replace('}', ""), title: name.replace('{', "").replace('}', ""), }, ); } } } fn load_sites(json: &Value, sites: Arc>>) { let arr = json .get("productions") .and_then(|v| v.as_array().map(|v| v.as_slice())) .unwrap_or(&[]); for s in arr { if let (Some(name), Some(id)) = ( s.get("tg_name").and_then(|v| v.as_str()), s.get("tg_slug").and_then(|v| v.as_str()), ) { Self::push_unique( &sites, FilterOption { id: id.into(), title: name.into(), }, ); } } } 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); } } } fn build_channel(&self, _: ClientVersion) -> Channel { Channel { id: "beeg".into(), name: "Beeg".into(), description: "Watch your favorite Porn on Beeg.com".into(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=beeg.com".into(), status: "active".into(), categories: vec![], options: vec![ ChannelOption { id: "sites".into(), title: "Sites".into(), description: "Filter for different Sites".into(), systemImage: "rectangle.stack".into(), colorName: "green".into(), options: self.sites.read().map(|v| v.clone()).unwrap_or_default(), multiSelect: false, }, ChannelOption { id: "categories".into(), title: "Categories".into(), description: "Filter for different Networks".into(), systemImage: "list.dash".into(), colorName: "purple".into(), options: self .categories .read() .map(|v| v.clone()) .unwrap_or_default(), multiSelect: false, }, ChannelOption { id: "stars".into(), title: "Stars".into(), description: "Filter for different Pornstars".into(), systemImage: "star.fill".into(), colorName: "yellow".into(), options: self.stars.read().map(|v| v.clone()).unwrap_or_default(), multiSelect: false, }, ], nsfw: true, cacheDuration: None, } } async fn get( &self, cache: VideoCache, page: u8, options: ServerOptions, ) -> Result> { let mut slug = ""; if let Some(categories) = options.categories.as_ref() { if !categories.is_empty() && categories != "all" { slug = categories; } } if let Some(sites) = options.sites.as_ref() { if !sites.is_empty() && sites != "all" { slug = sites; } } if let Some(stars) = options.stars.as_ref() { if !stars.is_empty() && stars != "all" { slug = stars; } } 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 { return Ok(items.clone()); } else { items.clone() } } None => { vec![] } }; let mut requester = crate::providers::requester_or_default(&options, module_path!(), "missing_requester"); let text = match requester.get(&video_url, None).await { Ok(text) => text, Err(e) => { report_provider_error_background("beeg", "get.request", &e.to_string()); return Ok(old_items); } }; let json: serde_json::Value = match serde_json::from_str::(&text) { Ok(json) => json, Err(e) => { report_provider_error_background("beeg", "get.parse_json", &e.to_string()); return Ok(old_items); } }; 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 = crate::providers::requester_or_default(&options, module_path!(), "missing_requester"); let text = match requester.get(&video_url, None).await { Ok(text) => text, Err(e) => { report_provider_error_background("beeg", "query.request", &e.to_string()); return Ok(old_items); } }; let json: serde_json::Value = match serde_json::from_str::(&text) { Ok(json) => json, Err(e) => { report_provider_error_background("beeg", "query.parse_json", &e.to_string()); return Ok(old_items); } }; 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::new(); let array = match json.as_array() { Some(a) => a, None => return items, }; for video in array { let file = match video.get("file") { Some(v) => v, None => continue, }; let hls = match file.get("hls_resources") { Some(v) => v, None => continue, }; let key = match hls.get("fl_cdn_multi").and_then(|v| v.as_str()) { Some(v) => v, None => continue, }; let id = file .get("id") .and_then(|v| v.as_i64()) .unwrap_or(0) .to_string(); let title = file .get("data") .and_then(|v| v.get(0)) .and_then(|v| v.get("cd_value")) .and_then(|v| v.as_str()) .map(|s| decode(s.as_bytes()).to_string().unwrap_or_default()) .unwrap_or_default(); let duration = file .get("fl_duration") .and_then(|v| v.as_u64()) .unwrap_or(0); let views = video .get("fc_facts") .and_then(|v| v.get(0)) .and_then(|v| v.get("fc_st_views")) .and_then(|v| v.as_str()) .and_then(|s| parse_abbreviated_number(s)) .unwrap_or(0); let thumb = format!( "https://thumbs.externulls.com/videos/{}/0.webp?size=480x270", id ); let mut item = VideoItem::new( id, title, format!("https://video.externulls.com/{}", key), "beeg".into(), thumb, duration as u32, ); if views > 0 { item = item.views(views); } items.push(item); } items } } #[async_trait] impl Provider for BeegProvider { async fn get_videos( &self, cache: VideoCache, _: DbPool, _: String, query: Option, page: String, _: String, options: ServerOptions, ) -> Vec { let page = page.parse::().unwrap_or(1); let result = match query { Some(q) => self.query(cache, page, &q, options).await, None => self.get(cache, page, options).await, }; result.unwrap_or_else(|e| { eprintln!("beeg provider error: {}", e); vec![] }) } fn get_channel(&self, clientversion: ClientVersion) -> Option { Some(self.build_channel(clientversion)) } }