use crate::DbPool; use crate::api::ClientVersion; use crate::db; 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::requester::Requester; use crate::videos::ServerOptions; use crate::videos::VideoItem; use async_trait::async_trait; use diesel::r2d2; use error_chain::error_chain; use futures::future::join_all; use htmlentity::entity::{ICodedDataTrait, decode}; use std::vec; use wreq::Version; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); JsonError(serde_json::Error); Pool(r2d2::Error); // Assuming r2d2 or similar for pool } errors { ParsingError(t: String) { description("parsing error") display("Parsing error: '{}'", t) } } } #[derive(Debug, Clone)] pub struct MissavProvider { url: String, } impl MissavProvider { pub fn new() -> Self { MissavProvider { url: "https://missav.ws".to_string(), } } fn build_channel(&self, _clientversion: ClientVersion) -> Channel { Channel { id: "missav".to_string(), name: "MissAV".to_string(), description: "Watch HD JAV Online".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=missav.ws".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: "released_at".to_string(), title: "Release Date".to_string(), }, FilterOption { id: "published_at".to_string(), title: "Recent Update".to_string(), }, FilterOption { id: "today_views".to_string(), title: "Today Views".to_string(), }, FilterOption { id: "weekly_views".to_string(), title: "Weekly Views".to_string(), }, FilterOption { id: "monthly_views".to_string(), title: "Monthly Views".to_string(), }, FilterOption { id: "views".to_string(), title: "Total Views".to_string(), }, ], multiSelect: false, }, ChannelOption { id: "filter".to_string(), title: "Filter".to_string(), description: "Filter the Videos".to_string(), systemImage: "line.horizontal.3.decrease.circle".to_string(), colorName: "green".to_string(), options: vec![ FilterOption { id: "new".to_string(), title: "Recent update".to_string(), }, FilterOption { id: "release".to_string(), title: "New Releases".to_string(), }, FilterOption { id: "uncensored-leak".to_string(), title: "Uncensored".to_string(), }, FilterOption { id: "english-subtitle".to_string(), title: "English subtitle".to_string(), }, ], multiSelect: false, }, ChannelOption { id: "language".to_string(), title: "Language".to_string(), description: "What Language to fetch".to_string(), systemImage: "flag.fill".to_string(), colorName: "gray".to_string(), options: vec![ FilterOption { id: "en".to_string(), title: "English".to_string(), }, FilterOption { id: "cn".to_string(), title: "简体中文".to_string(), }, FilterOption { id: "ja".to_string(), title: "日本語".to_string(), }, FilterOption { id: "ko".to_string(), title: "한국의".to_string(), }, FilterOption { id: "ms".to_string(), title: "Melayu".to_string(), }, FilterOption { id: "th".to_string(), title: "ไทย".to_string(), }, FilterOption { id: "de".to_string(), title: "Deutsch".to_string(), }, FilterOption { id: "fr".to_string(), title: "Français".to_string(), }, FilterOption { id: "vi".to_string(), title: "Tiếng Việt".to_string(), }, FilterOption { id: "id".to_string(), title: "Bahasa Indonesia".to_string(), }, FilterOption { id: "fil".to_string(), title: "Filipino".to_string(), }, FilterOption { id: "pt".to_string(), title: "Português".to_string(), }, ], multiSelect: false, }, ], nsfw: true, cacheDuration: None, } } async fn get( &self, cache: VideoCache, pool: DbPool, page: u8, mut sort: String, options: ServerOptions, ) -> Result> { // Use ok_or to avoid unwrapping options let language = options.language.as_ref().ok_or("Missing language")?; let filter = options.filter.as_ref().ok_or("Missing filter")?; let mut requester = options.requester.clone().ok_or("Missing requester")?; if !sort.is_empty() { sort = format!("&sort={}", sort); } let url_str = format!("{}/{}/{}?page={}{}", self.url, language, filter, page, sort); if let Some((time, items)) = cache.get(&url_str) { if time.elapsed().unwrap_or_default().as_secs() < 3600 { return Ok(items.clone()); } } let text = requester .get(&url_str, Some(Version::HTTP_2)) .await .unwrap_or_else(|e| { eprintln!("Error fetching Missav URL {}: {}", url_str, e); let _ = send_discord_error_report( e.to_string(), None, Some(&url_str), None, file!(), line!(), module_path!(), ); "".to_string() }); let video_items = self.get_video_items_from_html(text, pool, requester).await; if !video_items.is_empty() { cache.insert(url_str, video_items.clone()); } Ok(video_items) } async fn query( &self, cache: VideoCache, pool: DbPool, page: u8, query: &str, mut sort: String, options: ServerOptions, ) -> Result> { let language = options.language.as_ref().ok_or("Missing language")?; let mut requester = options.requester.clone().ok_or("Missing requester")?; let search_string = query.replace(" ", "%20"); if !sort.is_empty() { sort = format!("&sort={}", sort); } let url_str = format!( "{}/{}/search/{}?page={}{}", self.url, language, search_string, page, sort ); if let Some((time, items)) = cache.get(&url_str) { if time.elapsed().unwrap_or_default().as_secs() < 3600 { return Ok(items.clone()); } } let text = requester .get(&url_str, Some(Version::HTTP_2)) .await .unwrap_or_else(|e| { eprintln!("Error fetching Missav URL {}: {}", url_str, e); let _ = send_discord_error_report( e.to_string(), None, Some(&url_str), None, file!(), line!(), module_path!(), ); "".to_string() }); let video_items = self.get_video_items_from_html(text, pool, requester).await; if !video_items.is_empty() { cache.insert(url_str, video_items.clone()); } Ok(video_items) } async fn get_video_items_from_html( &self, html: String, pool: DbPool, requester: Requester, ) -> Vec { if html.is_empty() { return vec![]; } let segments: Vec<&str> = html.split("@mouseenter=\"setPreview(\'").collect(); if segments.len() < 2 { return vec![]; } let mut urls = vec![]; for video_segment in &segments[1..] { // Safer parsing: find start and end of href if let Some(start) = video_segment.find(" Result { // 1. Database Check { let mut conn = pool .get() .map_err(|e| Error::from(format!("Pool error: {}", e)))?; if let Ok(Some(entry)) = db::get_video(&mut conn, url_str.clone()) { if let Ok(video_item) = serde_json::from_str::(entry.as_str()) { return Ok(video_item); } } } // 2. Fetch Page let vid = requester .get(&url_str, Some(Version::HTTP_2)) .await .unwrap_or_else(|e| { eprintln!("Error fetching Missav URL {}: {}", url_str, e); let _ = send_discord_error_report( e.to_string(), None, Some(&url_str), None, file!(), line!(), module_path!(), ); "".to_string() }); // Helper closure to extract content between two strings let extract = |html: &str, start_tag: &str, end_tag: &str| -> Option { let start = html.find(start_tag)? + start_tag.len(); let rest = &html[start..]; let end = rest.find(end_tag)?; Some(rest[..end].to_string()) }; let mut title = extract(&vid, "().ok()) .unwrap_or(0); let id = url_str.split('/').last().ok_or("No ID found")?.to_string(); // 3. Extract Tags (Generic approach to avoid repetitive code) let mut tags = vec![]; for (label, prefix) in [ ("Actress:", "@actress"), ("Actor:", "@actor"), ("Maker:", "@maker"), ("Genre:", "@genre"), ] { let marker = format!("{}", label); if let Some(section) = extract(&vid, &marker, "") { for part in section.split("class=\"text-nord13 font-medium\">").skip(1) { if let Some(val) = part.split('<').next() { let clean = val.trim(); if !clean.is_empty() { tags.push(format!("{}:{}", prefix, clean)); } } } } } // 4. Extract Video URL (The m3u8 logic) let video_url = (|| { let parts_str = vid.split("m3u8").nth(1)?.split("https").next()?; let mut parts: Vec<&str> = parts_str.split('|').collect(); parts.reverse(); Some(format!( "https://{}.{}/{}-{}-{}-{}-{}/playlist.m3u8", parts.get(1)?, parts.get(2)?, parts.get(3)?, parts.get(4)?, parts.get(5)?, parts.get(6)?, parts.get(7)? )) })() .ok_or_else(|| ErrorKind::ParsingError(format!("video_url\n{:?}", vid).to_string()))?; let video_item = VideoItem::new(id, title, video_url, "missav".to_string(), thumb, duration) .tags(tags) .preview(format!( "https://fourhoi.com/{}/preview.mp4", url_str.split('/').last().unwrap_or_default() )); // 5. Cache to DB if let Ok(mut conn) = pool.get() { let _ = db::insert_video( &mut conn, &url_str, &serde_json::to_string(&video_item).unwrap_or_default(), ); } Ok(video_item) } } #[async_trait] impl Provider for MissavProvider { 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, pool, page_num, &q, sort, options).await, None => self.get(cache, pool, page_num, sort, options).await, }; result.unwrap_or_else(|e| { eprintln!("Error fetching videos: {}", e); let _ = send_discord_error_report( e.to_string(), Some(format_error_chain(&e)), None, None, file!(), line!(), module_path!(), ); vec![] }) } fn get_channel(&self, clientversion: ClientVersion) -> Option { Some(self.build_channel(clientversion)) } }