/*! GogoAnime (anitaku.to) streaming plugin for the BEX engine. Features: - Search anime with HTML scraping - Home page with Popular, Recent, New Season, Movies sections - Genre/category browsing (Action, Adventure, Comedy, Drama, etc.) - Fetch detailed info with episodes (title, image, description, type, status, genres) - Multiple server extraction (HSUB, SUB, DUB) from episode pages - HLS stream resolution via VibePlayer iframe JS parsing - Proper Referer/User-Agent headers for mpv playback */ #[allow(warnings)] mod bindings; use bindings::bex::plugin::common::*; use bindings::bex::plugin::http; use bindings::exports::api::Guest; struct Component; const BASE_URL: &str = "https://anitaku.to"; // ============================================================================ // Minimal HTML DOM parser (WASM-compatible, no external dep) // ============================================================================ struct Dom { html: String, } impl Dom { fn parse(html: &str) -> Self { Dom { html: html.to_string() } } #[allow(dead_code)] fn find_attr_by_id(&self, id: &str, attr: &str) -> Option { let id_pattern = format!("id=\"{}\"", id); let id_idx = self.html.find(&id_pattern)?; let tag_start = self.html[..id_idx].rfind('<')?; let tag_end = self.html[tag_start..].find('>')? + tag_start; let tag_str = &self.html[tag_start..tag_end]; extract_attr_value(tag_str, attr) } #[allow(dead_code)] fn find_attr(&self, tag: &str, attr: &str) -> Option { let pattern = format!("<{}", tag); let idx = self.html.find(&pattern)?; let tag_end = self.html[idx..].find('>')? + idx; let tag_str = &self.html[idx..tag_end]; extract_attr_value(tag_str, attr) } #[allow(dead_code)] fn find_class_text(&self, tag: &str, class: &str) -> Option { let pattern = format!("<{} class=\"{}\"", tag, class); let idx = self.html.find(&pattern)?; let content_start = self.html[idx..].find('>')? + idx + 1; let close = format!("", tag); let content_end = self.html[content_start..].find(&close)? + content_start; Some(self.html[content_start..content_end].trim().to_string()) } fn find_tag_content(&self, tag: &str) -> Option { let open_start = self.html.find(&format!("<{}", tag))?; let content_start = self.html[open_start..].find('>')? + open_start + 1; let close = format!("", tag); let content_end = self.html[content_start..].find(&close)? + content_start; Some(self.html[content_start..content_end].to_string()) } fn find_img_src(&self, container_class: &str) -> Option { let pattern = format!("class=\"{}\"", container_class); let idx = self.html.find(&pattern)?; let block_end = self.html[idx..].find("").unwrap_or(self.html.len() - idx) + idx; let block = &self.html[idx..block_end]; let img_idx = block.find("')? + img_idx; let img_tag = &block[img_idx..img_end]; extract_attr_value(img_tag, "src").or_else(|| extract_attr_value(img_tag, "data-src")) } } fn extract_attr_value(tag_str: &str, attr: &str) -> Option { let attr_pattern = format!("{}=\"", attr); if let Some(idx) = tag_str.find(&attr_pattern) { let val_start = idx + attr_pattern.len(); let val_end = tag_str[val_start..].find('"')? + val_start; Some(tag_str[val_start..val_end].to_string()) } else { None } } // ============================================================================ // HTTP helpers — delegate to host // ============================================================================ impl Component { fn get_headers() -> Vec { vec![ Attr { key: "User-Agent".to_string(), value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36".to_string() }, Attr { key: "Connection".to_string(), value: "keep-alive".to_string() }, Attr { key: "Accept".to_string(), value: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string() }, Attr { key: "Accept-Language".to_string(), value: "en-US,en;q=0.5".to_string() }, Attr { key: "Referer".to_string(), value: format!("{}/", BASE_URL) }, ] } fn fetch_url(url: &str) -> Result { Self::fetch_url_with_headers(url, Self::get_headers()) } fn fetch_url_with_headers(url: &str, headers: Vec) -> Result { let req = http::Request { method: http::Method::Get, url: url.to_string(), headers, body: None, timeout_ms: Some(15000), follow_redirects: true, cache_mode: http::CacheMode::NoStore, max_bytes: Some(5 * 1024 * 1024), }; match http::send_request(&req) { Ok(resp) if resp.status == 200 => String::from_utf8(resp.body) .map_err(|e| PluginError::Parse(format!("UTF-8 error: {}", e))), Ok(resp) => Err(PluginError::Network(format!("HTTP {}", resp.status))), Err(e) => Err(e), } } // ======================================================================== // Scrape anime cards from search/browse listing pages // ======================================================================== /// Parse `ul.items li` cards from GogoAnime search/category pages. /// Each card has: `.name a[href]` (title + link), `.img img[src]` (poster), /// `.released` (year info). fn scrape_anime_cards(html: &str) -> Vec { let mut results = Vec::new(); let mut seen_ids = std::collections::HashSet::new(); // GogoAnime search/category pages use