use crate::DbPool; use crate::api::ClientVersion; use crate::providers::Provider; use crate::status::*; use crate::util::cache::VideoCache; use crate::util::discord::{format_error_chain, send_discord_error_report}; use crate::util::parse_abbreviated_number; 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::vec; pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata = crate::providers::ProviderChannelMetadata { group_id: "mainstream-tube", tags: &["mainstream", "legacy", "studio"], }; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); } } #[derive(Debug, Clone)] pub struct TnaflixProvider { url: String, } impl TnaflixProvider { pub fn new() -> Self { TnaflixProvider { url: "https://www.tnaflix.com".to_string(), } } fn build_channel(&self, _clientversion: ClientVersion) -> Channel { Channel { id: "tnaflix".to_string(), name: "TnAflix".to_string(), description: "Just Tits and Ass".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=www.tnaflix.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: "featured".into(), title: "Featured".into(), }, FilterOption { id: "toprated".into(), title: "Top Rated".into(), }, ], multiSelect: false, }, ChannelOption { id: "duration".to_string(), title: "Duration".to_string(), description: "Length of the Videos".to_string(), systemImage: "timer".to_string(), colorName: "green".to_string(), options: vec![ FilterOption { id: "all".into(), title: "All".into(), }, FilterOption { id: "short".into(), title: "Short (1-3 min)".into(), }, FilterOption { id: "medium".into(), title: "Medium (3-10 min)".into(), }, FilterOption { id: "long".into(), title: "Long (10-30 min)".into(), }, FilterOption { id: "full".into(), title: "Full length (30+ min)".into(), }, ], multiSelect: false, }, ], nsfw: true, cacheDuration: None, } } async fn get( &self, cache: VideoCache, page: u8, sort: &str, options: ServerOptions, ) -> Result> { let sort_string = match sort { "featured" => "featured", "toprated" => "toprated", _ => "new", }; let duration_string = options .duration .clone() .unwrap_or_else(|| "all".to_string()); let video_url = format!( "{}/{}/{}?d={}", self.url, sort_string, page, duration_string ); // Cache Logic if let Some((time, items)) = cache.get(&video_url) { if time.elapsed().unwrap_or_default().as_secs() < 300 { return Ok(items.clone()); } } let mut requester = options.requester.clone().ok_or("Requester missing")?; let text = requester .get(&video_url, None) .await .map_err(|e| format!("{}", e))?; let video_items = self.get_video_items_from_html(text); if !video_items.is_empty() { cache.insert(video_url, video_items.clone()); } Ok(video_items) } async fn query( &self, cache: VideoCache, page: u8, query: &str, options: ServerOptions, ) -> Result> { let search_string = query.to_lowercase().trim().replace(" ", "+"); let duration_string = options .duration .clone() .unwrap_or_else(|| "all".to_string()); let video_url = format!( "{}/search?what={}&d={}&page={}", self.url, search_string, duration_string, page ); if let Some((time, items)) = cache.get(&video_url) { if time.elapsed().unwrap_or_default().as_secs() < 300 { return Ok(items.clone()); } } let mut requester = options.requester.clone().ok_or("Requester missing")?; let text = requester .get(&video_url, None) .await .map_err(|e| format!("{}", e))?; let video_items = self.get_video_items_from_html(text); if !video_items.is_empty() { cache.insert(video_url, video_items.clone()); } Ok(video_items) } fn get_video_items_from_html(&self, html: String) -> Vec { if html.is_empty() { return vec![]; } let mut items = Vec::new(); // Safe helper for splitting let get_part = |input: &str, sep: &str, idx: usize| -> Option { input.split(sep).nth(idx).map(|s| s.to_string()) }; // Navigate to the video list container safely let list_part = match html.split("row video-list").nth(1) { Some(p) => match p.split("pagination ").next() { Some(inner) => inner, None => return vec![], }, None => return vec![], }; let raw_videos: Vec<&str> = list_part .split("col-xs-6 col-md-4 col-xl-3 mb-3") .skip(1) .collect(); for (idx, segment) in raw_videos.iter().enumerate() { let item: Option = (|| { let video_url = get_part(segment, " href=\"", 1)? .split("\"") .next()? .to_string(); let mut title = get_part(segment, "class=\"video-title text-break\">", 1)? .split("<") .next()? .trim() .to_string(); title = decode(title.as_bytes()).to_string().unwrap_or(title); let id = video_url.split("/").nth(5)?.to_string(); let thumb = if segment.contains("data-src=\"") { get_part(segment, "data-src=\"", 1)? .split("\"") .next()? .to_string() } else { get_part(segment, "", 1)? .split("<") .next()? .to_string(); let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; let views = if segment.contains("icon-eye\">") { let v_str = get_part(segment, "icon-eye\">", 1)? .split("<") .next()? .trim() .to_string(); parse_abbreviated_number(&v_str).unwrap_or(0) as u32 } else { 0 }; let preview = get_part(segment, "data-trailer=\"", 1)? .split("\"") .next()? .to_string(); Some( VideoItem::new(id, title, video_url, "tnaflix".to_string(), thumb, duration) .views(views) .preview(preview), ) })(); if let Some(v) = item { items.push(v); } else { eprintln!("Tnaflix: Failed to parse item index {}", idx); tokio::spawn(async move { let _ = send_discord_error_report( format!("Tnaflix Parse Error at index {}", idx), None, Some("Tnaflix Provider"), None, file!(), line!(), module_path!(), ) .await; }); } } items } } #[async_trait] impl Provider for TnaflixProvider { async fn get_videos( &self, cache: VideoCache, _pool: DbPool, sort: String, query: Option, page: String, _per_page: String, options: ServerOptions, ) -> Vec { let page_num = page.parse::().unwrap_or(1); let result = match query { Some(q) => self.query(cache, page_num, &q, options).await, None => self.get(cache, page_num, &sort, options).await, }; match result { Ok(v) => v, Err(e) => { eprintln!("Tnaflix Error: {}", e); // 1. Create a collection of owned data so we don't hold references to `e` let mut error_reports = Vec::new(); // Iterating through the error chain to collect data into owned Strings for cause in e.iter().skip(1) { error_reports.push(( cause.to_string(), // Title format_error_chain(cause), // Description/Chain format!("caused by: {}", cause), // Message )); } // 2. Now that we aren't holding any `&dyn StdError`, we can safely .await for (title, chain_str, msg) in error_reports { let _ = send_discord_error_report( title, Some(chain_str), Some("Pornzog Provider"), Some(&msg), file!(), line!(), module_path!(), ) .await; } // In a real app, you'd extract owned strings here // and await your discord reporter as we did for Pornzog vec![] } } } fn get_channel(&self, clientversion: ClientVersion) -> Option { Some(self.build_channel(clientversion)) } }