use crate::DbPool; use crate::api::ClientVersion; use crate::db; use crate::providers::{Provider, report_provider_error, report_provider_error_background}; use crate::status::*; use crate::util::cache::VideoCache; use crate::util::time::parse_time_to_seconds; use crate::videos::ServerOptions; use crate::videos::{self, VideoItem}; use async_trait::async_trait; use error_chain::error_chain; use futures::future::join_all; use htmlentity::entity::{ICodedDataTrait, decode}; use serde::Deserialize; use serde::Serialize; use wreq::Client; use wreq::Version; use wreq_util::Emulation; pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata = crate::providers::ProviderChannelMetadata { group_id: "studio-network", tags: &["regional", "amateur", "mixed"], }; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); JsonError(serde_json::Error); } } #[derive(Debug, Deserialize, Serialize)] struct PerverzijaDbEntry { url_string: String, tags_strings: Vec, } #[derive(Debug, Clone)] pub struct PerverzijaProvider { url: String, } impl PerverzijaProvider { pub fn new() -> Self { PerverzijaProvider { url: "https://tube.perverzija.com/".to_string(), } } fn build_channel(&self, clientversion: ClientVersion) -> Channel { let _ = clientversion; Channel { id: "perverzija".to_string(), name: "Perverzija".to_string(), description: "Free videos from Perverzija".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube.perverzija.com" .to_string(), status: "active".to_string(), categories: vec![], options: vec![ChannelOption { id: "featured".to_string(), title: "Featured".to_string(), description: "Filter Featured Videos.".to_string(), systemImage: "star".to_string(), colorName: "red".to_string(), options: vec![ FilterOption { id: "all".to_string(), title: "No".to_string(), }, FilterOption { id: "featured".to_string(), title: "Yes".to_string(), }, ], multiSelect: false, }], nsfw: true, cacheDuration: None, } } fn extract_between<'a>(haystack: &'a str, start: &str, end: &str) -> Option<&'a str> { let rest = haystack.split(start).nth(1)?; Some(rest.split(end).next().unwrap_or_default()) } fn extract_iframe_src(haystack: &str) -> String { Self::extract_between(haystack, "iframe src=\"", "\"") .or_else(|| Self::extract_between(haystack, "iframe src="", """)) .unwrap_or_default() .to_string() } fn extract_thumb(haystack: &str) -> String { let img_segment = haystack.split(" String { let mut title = Self::extract_between(haystack, "

", "

") .or_else(|| Self::extract_between(haystack, "

", "

")) .or_else(|| Self::extract_between(haystack, " title='", "'")) .or_else(|| Self::extract_between(haystack, " title=\"", "\"")) .unwrap_or_default() .to_string(); title = decode(title.as_bytes()).to_string().unwrap_or(title); if title.contains('<') && title.contains('>') { let mut plain = String::new(); let mut in_tag = false; for c in title.chars() { match c { '<' => in_tag = true, '>' => in_tag = false, _ if !in_tag => plain.push(c), _ => {} } } let normalized = plain.split_whitespace().collect::>().join(" "); if !normalized.is_empty() { title = normalized; } } else { title = title.split_whitespace().collect::>().join(" "); } title.trim().to_string() } async fn get( &self, cache: VideoCache, pool: DbPool, page: u8, options: ServerOptions, ) -> Result> { let featured = options.featured.clone().unwrap_or("".to_string()); let mut prefix_uri = "".to_string(); if featured == "featured" { prefix_uri = "featured-scenes/".to_string(); } let mut url_str = format!("{}{}page/{}/", self.url, prefix_uri, page); if page == 1 { url_str = format!("{}{}", self.url, prefix_uri); } let old_items = match cache.get(&url_str) { Some((time, items)) => { if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 { //println!("Cache hit for URL: {}", url_str); 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(&url_str, Some(Version::HTTP_2)).await { Ok(text) => text, Err(e) => { report_provider_error( "perverzija", "get.request", &format!("url={url_str}; error={e}"), ) .await; return Ok(old_items); } }; let video_items: Vec = self.get_video_items_from_html(text.clone(), pool); if !video_items.is_empty() { cache.remove(&url_str); cache.insert(url_str.clone(), video_items.clone()); } else { return Ok(old_items); } Ok(video_items) } async fn query( &self, cache: VideoCache, pool: DbPool, page: u8, query: &str, options: ServerOptions, ) -> Result> { let mut query_parse = true; let search_string = query.replace(" ", "+"); let mut url_str = format!("{}page/{}/?s={}", self.url, page, search_string); if page == 1 { url_str = format!("{}?s={}", self.url, search_string); } if query.starts_with("@studio:") { let studio_name = query.replace("@studio:", ""); url_str = format!("{}studio/{}/page/{}/", self.url, studio_name, page); query_parse = false; } else if query.starts_with("@stars:") { let stars_name = query.replace("@stars:", ""); url_str = format!("{}stars/{}/page/{}/", self.url, stars_name, page); query_parse = false; } url_str = url_str.replace("page/1/", ""); // Check our Video Cache. If the result is younger than 1 hour, we return it. let old_items = match cache.get(&url_str) { Some((time, items)) => { if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 { 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(&url_str, Some(Version::HTTP_2)).await { Ok(text) => text, Err(e) => { report_provider_error( "perverzija", "query.request", &format!("url={url_str}; error={e}"), ) .await; return Ok(old_items); } }; let video_items: Vec = match query_parse { true => { self.get_video_items_from_html_query(text.clone(), pool) .await } false => self.get_video_items_from_html(text.clone(), pool), }; if !video_items.is_empty() { cache.remove(&url_str); cache.insert(url_str.clone(), video_items.clone()); } else { return Ok(old_items); } Ok(video_items) } fn get_video_items_from_html(&self, html: String, pool: DbPool) -> Vec { if html.is_empty() { report_provider_error_background( "perverzija", "get_video_items_from_html.empty_html", "empty html response", ); return vec![]; } let mut items: Vec = Vec::new(); let video_listing_content = html.split("video-listing-content").nth(1).unwrap_or(&html); let raw_videos: Vec<&str> = video_listing_content .split("video-item post") .skip(1) .collect(); if raw_videos.is_empty() { report_provider_error_background( "perverzija", "get_video_items_from_html.no_segments", &format!("html_len={}", html.len()), ); return vec![]; } for video_segment in raw_videos { let title = Self::extract_title(video_segment); let embed_html_raw = Self::extract_between(video_segment, "data-embed='", "'") .or_else(|| Self::extract_between(video_segment, "data-embed=\"", "\"")) .unwrap_or_default() .to_string(); let embed_html = decode(embed_html_raw.as_bytes()) .to_string() .unwrap_or(embed_html_raw.clone()); let mut url_str = Self::extract_iframe_src(&embed_html); if url_str.is_empty() { url_str = Self::extract_iframe_src(video_segment); } if url_str.is_empty() { report_provider_error_background( "perverzija", "get_video_items_from_html.url_missing", "missing iframe src in segment", ); continue; } url_str = url_str.replace("index.php", "xs1.php"); if url_str.starts_with("https://streamtape.com/") { continue; // Skip Streamtape links } let id_url = Self::extract_between(video_segment, "data-url='", "'") .or_else(|| Self::extract_between(video_segment, "data-url=\"", "\"")) .unwrap_or_default() .to_string(); let mut id = url_str .split("data=") .nth(1) .unwrap_or_default() .split('&') .next() .unwrap_or_default() .to_string(); if id.is_empty() { id = id_url .trim_end_matches('/') .rsplit('/') .next() .unwrap_or_default() .to_string(); } let raw_duration = Self::extract_between(video_segment, "time_dur\">", "<") .or_else(|| Self::extract_between(video_segment, "class=\"time\">", "<")) .unwrap_or("00:00") .to_string(); let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; let thumb = Self::extract_thumb(video_segment); match pool.get() { Ok(mut conn) => { if !id_url.is_empty() { let _ = db::insert_video(&mut conn, &id_url, &url_str); } } Err(e) => { report_provider_error_background( "perverzija", "get_video_items_from_html.insert_video.pool_get", &e.to_string(), ); } } let referer_url = "https://xtremestream.xyz/".to_string(); let mut tags: Vec = Vec::new(); let studios_parts = video_segment.split("a href=\"").collect::>(); for studio in studios_parts.iter().skip(1) { if studio.starts_with("https://tube.perverzija.com/studio/") { tags.push( studio .split("/\"") .collect::>() .get(0) .copied() .unwrap_or_default() .replace("https://tube.perverzija.com/studio/", "@studio:") .to_string(), ); } } for tag in video_segment.split_whitespace() { let token = tag.trim_matches(|c: char| c == '"' || c == '\'' || c == '>' || c == '<'); if token.starts_with("stars-") { let tag_name = token .split("stars-") .nth(1) .unwrap_or_default() .split('"') .next() .unwrap_or_default() .to_string(); if !tag_name.is_empty() { tags.push(format!("@stars:{}", tag_name)); } } } for tag in video_segment.split_whitespace() { let token = tag.trim_matches(|c: char| c == '"' || c == '\'' || c == '>' || c == '<'); if token.starts_with("tag-") { let tag_name = token.split("tag-").nth(1).unwrap_or_default().to_string(); if !tag_name.is_empty() { tags.push(tag_name.replace("-", " ").to_string()); } } } let mut video_item = VideoItem::new( id, title, url_str.clone(), "perverzija".to_string(), thumb, duration, ) .tags(tags); // .embed(embed.clone()); let mut format = videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string()); format.add_http_header("Referer".to_string(), referer_url.clone()); if let Some(formats) = video_item.formats.as_mut() { formats.push(format); } else { video_item.formats = Some(vec![format]); } items.push(video_item); } return items; } async fn get_video_items_from_html_query(&self, html: String, pool: DbPool) -> Vec { let raw_videos: Vec<&str> = html.split("video-item post").skip(1).collect(); if raw_videos.is_empty() { report_provider_error_background( "perverzija", "get_video_items_from_html_query.no_segments", &format!("html_len={}", html.len()), ); return vec![]; } let futures = raw_videos .into_iter() .map(|el| self.get_video_item(el, pool.clone())); let results: Vec> = join_all(futures).await; let items: Vec = results.into_iter().filter_map(Result::ok).collect(); return items; } async fn get_video_item(&self, snippet: &str, pool: DbPool) -> Result { if snippet.trim().is_empty() { report_provider_error_background( "perverzija", "get_video_item.empty_snippet", "snippet is empty", ); return Err("empty snippet".into()); } let title = Self::extract_title(snippet); let thumb = Self::extract_thumb(snippet); let duration = 0; let lookup_url = Self::extract_between(snippet, " href=\"", "\"") .or_else(|| Self::extract_between(snippet, "data-url='", "'")) .unwrap_or_default() .to_string(); if lookup_url.is_empty() { report_provider_error_background( "perverzija", "get_video_item.lookup_url_missing", "missing lookup url in snippet", ); return Err("Failed to parse lookup url".into()); } let referer_url = "https://xtremestream.xyz/".to_string(); let mut conn = match pool.get() { Ok(conn) => conn, Err(e) => { report_provider_error("perverzija", "get_video_item.pool_get", &e.to_string()) .await; return Err("couldn't get db connection from pool".into()); } }; let db_result = db::get_video(&mut conn, lookup_url.clone()); match db_result { Ok(Some(entry)) => { if entry.starts_with("{") { // replace old urls with new json objects let entry = serde_json::from_str::(entry.as_str())?; let url_str = entry.url_string; let tags = entry.tags_strings; if url_str.starts_with("!") { return Err("Video was removed".into()); } let mut id = url_str .split("data=") .collect::>() .get(1) .copied() .unwrap_or_default() .to_string(); if id.contains("&") { id = id .split("&") .collect::>() .get(0) .copied() .unwrap_or_default() .to_string() } let mut video_item = VideoItem::new( id, title, url_str.clone(), "perverzija".to_string(), thumb, duration, ) .tags(tags); let mut format = videos::VideoFormat::new( url_str.clone(), "1080".to_string(), "m3u8".to_string(), ); format.add_http_header("Referer".to_string(), referer_url.clone()); if let Some(formats) = video_item.formats.as_mut() { formats.push(format); } else { video_item.formats = Some(vec![format]); } return Ok(video_item); } else { let _ = db::delete_video(&mut conn, lookup_url.clone()); }; } Ok(None) => {} Err(e) => { println!("Error fetching video from database: {}", e); // return Err(format!("Error fetching video from database: {}", e).into()); } } drop(conn); let client = Client::builder().emulation(Emulation::Firefox136).build()?; let response = client.get(lookup_url.clone()).send().await?; let text = match response.status().is_success() { true => response.text().await?, false => { println!("Failed to fetch video details"); return Err("Failed to fetch video details".into()); } }; let mut url_str = text .split("