Files
asepharyana-hub-scraper/src/modules/anime2/scraping.rs
T

360 lines
12 KiB
Rust

use crate::shared::types::entities::anime::*;
use crate::shared::utils::parse_html;
use crate::shared::utils::scraping::{
attr, attr_from, attr_from_or, extract_slug, selector, text, text_from_or,
};
use scraper::{Html, Selector};
// ============================================================================
// SELECTORS
// ============================================================================
/// Common selectors used across anime parsing
pub struct AnimeSelectors {
pub item: Selector,
pub title: Selector,
pub link: Selector,
pub img: Selector,
pub episode: Selector,
pub score: Selector,
pub status: Selector,
pub genre: Selector,
pub rating: Selector,
pub type_sel: Selector,
pub season: Selector,
pub desc: Selector,
}
impl AnimeSelectors {
pub fn new() -> Result<Self, String> {
Ok(Self {
item: selector("article.bs").ok_or("Invalid selector: article.bs")?,
title: selector(".tt h2").ok_or("Invalid selector: .tt h2")?,
link: selector("a").ok_or("Invalid selector: a")?,
img: selector("img").ok_or("Invalid selector: img")?,
episode: selector(".epx").ok_or("Invalid selector: .epx")?,
score: selector(".numscore").ok_or("Invalid selector: .numscore")?,
status: selector(".status").ok_or("Invalid selector: .status")?,
genre: selector(".genres a").ok_or("Invalid selector: .genres a")?,
rating: selector(".score").ok_or("Invalid selector: .score")?,
type_sel: selector(".typez").ok_or("Invalid selector: .typez")?,
season: selector(".season").ok_or("Invalid selector: .season")?,
desc: selector(".data .typez").ok_or("Invalid selector: .data .typez")?,
})
}
}
impl Default for AnimeSelectors {
fn default() -> Self {
Self::new().expect("Valid CSS selectors")
}
}
// Global lazy static instance for selectors to avoid reallocation per parse
use once_cell::sync::Lazy;
static ANIME_SELECTORS: Lazy<Result<AnimeSelectors, String>> =
Lazy::new(|| AnimeSelectors::new().map_err(|e| format!("Failed to create selectors: {}", e)));
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/// Extract poster URL from an element, checking both src and data-src attributes
pub fn extract_poster(element: &scraper::ElementRef, img_selector: &Selector) -> String {
element
.select(img_selector)
.next()
.and_then(|e| attr(&e, "src").or(attr(&e, "data-src")))
.unwrap_or_default()
}
// ============================================================================
// ANIME PARSERS
// ============================================================================
/// Parse ongoing anime items from HTML
pub fn parse_ongoing_anime(
html: &str,
) -> Result<Vec<OngoingAnimeItem>, Box<dyn std::error::Error + Send + Sync>> {
let document = parse_html(html);
let selectors = ANIME_SELECTORS.as_ref().map_err(|e| e.clone())?;
let mut items = Vec::new();
for element in document.select(&selectors.item) {
let title = text_from_or(&element, &selectors.title, "");
if title.is_empty() {
continue;
}
let href = attr_from_or(&element, &selectors.link, "href", "");
let slug = extract_slug(&href);
let poster = extract_poster(&element, &selectors.img);
let current_episode = text_from_or(&element, &selectors.episode, "N/A");
let anime_url = attr_from_or(&element, &selectors.link, "href", "");
items.push(OngoingAnimeItem {
title,
slug,
poster,
current_episode,
anime_url,
});
}
Ok(items)
}
/// Parse ongoing anime items with score from HTML
pub fn parse_ongoing_anime_with_score(
html: &str,
) -> Result<Vec<OngoingAnimeItemWithScore>, Box<dyn std::error::Error + Send + Sync>> {
let document = parse_html(html);
let selectors = ANIME_SELECTORS.as_ref().map_err(|e| e.clone())?;
let mut items = Vec::new();
for element in document.select(&selectors.item) {
let title = text_from_or(&element, &selectors.title, "");
if title.is_empty() {
continue;
}
let poster = extract_poster(&element, &selectors.img);
let score = text_from_or(&element, &selectors.score, "N/A");
let anime_url = attr_from_or(&element, &selectors.link, "href", "");
let slug = extract_slug(&anime_url);
items.push(OngoingAnimeItemWithScore {
title,
slug,
poster,
score,
anime_url,
});
}
Ok(items)
}
/// Parse complete anime items from HTML
pub fn parse_complete_anime(
html: &str,
) -> Result<Vec<CompleteAnimeItem>, Box<dyn std::error::Error + Send + Sync>> {
let document = parse_html(html);
let selectors = ANIME_SELECTORS.as_ref().map_err(|e| e.clone())?;
let mut items = Vec::new();
for element in document.select(&selectors.item) {
let title = text_from_or(&element, &selectors.title, "");
if title.is_empty() {
continue;
}
let href = attr_from_or(&element, &selectors.link, "href", "");
let slug = extract_slug(&href);
let poster = extract_poster(&element, &selectors.img);
let episode_count = text_from_or(&element, &selectors.episode, "N/A");
let anime_url = attr_from_or(&element, &selectors.link, "href", "");
items.push(CompleteAnimeItem {
title,
slug,
poster,
episode_count,
anime_url,
});
}
Ok(items)
}
/// Parse latest anime items from HTML
pub fn parse_latest_anime(
html: &str,
) -> Result<Vec<LatestAnimeItem>, Box<dyn std::error::Error + Send + Sync>> {
let document = parse_html(html);
let selectors = ANIME_SELECTORS.as_ref().map_err(|e| e.clone())?;
let mut items = Vec::new();
for element in document.select(&selectors.item) {
let title = text_from_or(&element, &selectors.title, "");
if title.is_empty() {
continue;
}
let poster = extract_poster(&element, &selectors.img);
let current_episode = text_from_or(&element, &selectors.episode, "N/A");
let score = text_from_or(&element, &selectors.score, "N/A");
let anime_url = attr_from_or(&element, &selectors.link, "href", "");
let slug = extract_slug(&anime_url);
items.push(LatestAnimeItem {
title,
slug,
poster,
current_episode,
score,
anime_url,
});
}
Ok(items)
}
/// Parse search results from HTML
pub fn parse_search_anime(
html: &str,
) -> Result<Vec<SearchAnimeItem>, Box<dyn std::error::Error + Send + Sync>> {
let document = parse_html(html);
let selectors = ANIME_SELECTORS.as_ref().map_err(|e| e.clone())?;
let mut items = Vec::new();
for element in document.select(&selectors.item) {
let title = text_from_or(&element, &selectors.title, "");
if title.is_empty() {
continue;
}
let href = attr_from(&element, &selectors.link, "href").unwrap_or_default();
let slug = extract_slug(&href);
let poster = extract_poster(&element, &selectors.img);
let description = text_from_or(&element, &selectors.desc, "");
let anime_url = attr_from_or(&element, &selectors.link, "href", "");
let genres = element.select(&selectors.genre).map(|e| text(&e)).collect();
let rating = text_from_or(&element, &selectors.rating, "");
let r#type = text_from_or(&element, &selectors.type_sel, "");
let season = text_from_or(&element, &selectors.season, "");
items.push(SearchAnimeItem {
title,
slug,
poster,
description,
anime_url,
genres,
rating,
r#type,
season,
});
}
Ok(items)
}
/// Parse genre-filtered anime items from HTML
pub fn parse_genre_anime(
html: &str,
) -> Result<Vec<GenreAnimeItem>, Box<dyn std::error::Error + Send + Sync>> {
let document = parse_html(html);
let selectors = ANIME_SELECTORS.as_ref().map_err(|e| e.clone())?;
let mut items = Vec::new();
for element in document.select(&selectors.item) {
let title = text_from_or(&element, &selectors.title, "");
if title.is_empty() {
continue;
}
let poster = extract_poster(&element, &selectors.img);
let score = text_from_or(&element, &selectors.score, "N/A");
let status = text_from_or(&element, &selectors.status, "Unknown");
let anime_url = attr_from_or(&element, &selectors.link, "href", "");
let slug = extract_slug(&anime_url);
items.push(GenreAnimeItem {
title,
slug,
poster,
score,
status,
anime_url,
});
}
Ok(items)
}
// ============================================================================
// PAGINATION PARSERS
// ============================================================================
/// Parse pagination from HTML document
pub fn parse_pagination(document: &Html, current_page: u32) -> Result<Pagination, String> {
let pagination_selector =
selector(".pagination .page-numbers:not(.next)").ok_or("Invalid selector")?;
let next_selector = selector(".pagination .next").ok_or("Invalid selector")?;
let last_visible_page = document
.select(&pagination_selector)
.next_back()
.and_then(|e| text(&e).trim().parse::<u32>().ok())
.unwrap_or(current_page);
let has_next_page = document.select(&next_selector).next().is_some();
let next_page = if has_next_page {
Some(current_page + 1)
} else {
None
};
let has_previous_page = current_page > 1;
let previous_page = if has_previous_page {
Some(current_page - 1)
} else {
None
};
Ok(Pagination {
current_page,
last_visible_page,
has_next_page,
next_page,
has_previous_page,
previous_page,
})
}
/// Parse pagination with string-based page numbers (for search results)
pub fn parse_pagination_with_string(
document: &Html,
current_page: u32,
) -> Result<PaginationWithStringPages, String> {
let pagination_selector =
selector(".pagination .page-numbers:not(.next)").ok_or("Invalid selector")?;
let next_selector = selector(".pagination .next").ok_or("Invalid selector")?;
let last_visible_page = document
.select(&pagination_selector)
.last()
.and_then(|e| text(&e).trim().parse::<u32>().ok())
.unwrap_or(current_page);
let has_next_page = document.select(&next_selector).next().is_some();
let next_page = if has_next_page {
document
.select(&next_selector)
.next()
.and_then(|e| attr(&e, "href"))
.and_then(|href| href.split("/page/").nth(1).map(|s| s.to_string()))
.and_then(|s| s.split('/').next().map(|s| s.to_string()))
} else {
None
};
let has_previous_page = current_page > 1;
let previous_page = if has_previous_page {
Some((current_page - 1).to_string())
} else {
None
};
Ok(PaginationWithStringPages {
current_page,
last_visible_page,
has_next_page,
next_page,
has_previous_page,
previous_page,
})
}