From e6d75edf3384f518acd444a34d3da5d90a497c78 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 31 Aug 2026 20:59:20 +0700 Subject: [PATCH] fix(anime): repair genre & latest endpoints for the new otakudesu layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current otakudesu site changed paths and card layout, breaking two APIs: 1. genre pages: singular /genre/{slug}/page/{n}/ 301-redirects to a dead otakudesu.io placeholder → 0 items. Fix: plural /genres/{slug}/page/{n}/. Also re-target parse_genre_anime_document from the retired .venz ul li / .thumbz h2.jdlflm layout to the new .col-anime card layout (.col-anime-title a, .col-anime-eps, .col-anime-rating, .col-anime-cover img). 2. latest: /latest-anime/ was removed (301 → otakudesu.io). The homepage IS the latest-episodes feed (.venz ul li with .thumbz h2.jdlflm/.epz), so fetch_latest_anime_page now uses base_url() for page 1 and /page/N/ for later pages instead of the dead path. --- src/infrastructure/repository/otakudesu.rs | 14 +++++++-- .../repository/parsers/otakudesu_parser.rs | 30 +++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/infrastructure/repository/otakudesu.rs b/src/infrastructure/repository/otakudesu.rs index e99af74..58cc34c 100644 --- a/src/infrastructure/repository/otakudesu.rs +++ b/src/infrastructure/repository/otakudesu.rs @@ -56,7 +56,10 @@ impl OtakudesuRepository { } pub fn genre_page_url(&self, genre_slug: &str, page: &str) -> String { - format!("{}/genre/{}/page/{}/", self.base_url(), genre_slug, page) + // Current otakudesu uses the plural `/genres/{slug}/` path (archive + // layout). The singular `/genre/{slug}/page/{page}/` path 301-redirects + // to otakudesu.io (a 404 placeholder), which made genre pages return 0. + format!("{}/genres/{}/page/{}/", self.base_url(), genre_slug, page) } pub fn full_episode_url(&self, slug: &str) -> String { @@ -144,7 +147,14 @@ impl OtakudesuRepository { &self, slug: &str, ) -> Result<(Vec, Pagination), ScrapingError> { - let url = self.page_url("latest-anime", slug); + // The site's `/latest-anime/` path was removed (301 → otakudesu.io + // placeholder). The homepage IS the latest-episodes page (`.venz ul li` + // with `.thumbz h2.jdlflm`, `.epz`), paginated via WordPress `/page/N/`. + let url = if slug == "1" { + format!("{}/", self.base_url()) + } else { + format!("{}/page/{}/", self.base_url(), slug) + }; let html = self.fetch_html(&url).await?; let slug_owned = slug.to_string(); tokio::task::spawn_blocking(move || { diff --git a/src/infrastructure/repository/parsers/otakudesu_parser.rs b/src/infrastructure/repository/parsers/otakudesu_parser.rs index 704df8e..e6059b5 100644 --- a/src/infrastructure/repository/parsers/otakudesu_parser.rs +++ b/src/infrastructure/repository/parsers/otakudesu_parser.rs @@ -520,23 +520,29 @@ pub fn parse_genre_anime_document( let document = parse_html(html); let mut items = Vec::new(); - let venz_sel = selector(".venz ul li") - .ok_or_else(|| ScrapingError::Parse("Failed to parse .venz ul li selector".into()))?; - let title_sel = selector(".thumbz h2.jdlflm") + // Current otakudesu genre archive pages use the `.col-anime` card layout + // (`.col-md-4 .col-anime`), each with `.col-anime-title a`, `.col-anime-eps`, + // `.col-anime-rating`, `.col-anime-cover img` and a synopsis. The old + // `.venz ul li` / `.thumbz h2.jdlflm` / `.epz` layout no longer appears on + // these pages, which made genre filtering silently return 0 items. + let card_sel = selector(".col-anime") + .ok_or_else(|| ScrapingError::Parse("Failed to parse .col-anime selector".into()))?; + let title_sel = selector(".col-anime-title a") .ok_or_else(|| ScrapingError::Parse("Failed to parse title selector".into()))?; - let link_sel = selector("a") - .ok_or_else(|| ScrapingError::Parse("Failed to parse link selector".into()))?; - let img_sel = selector("img") + let eps_sel = selector(".col-anime-eps") + .ok_or_else(|| ScrapingError::Parse("Failed to parse eps selector".into()))?; + let rating_sel = selector(".col-anime-rating") + .ok_or_else(|| ScrapingError::Parse("Failed to parse rating selector".into()))?; + let img_sel = selector(".col-anime-cover img") .ok_or_else(|| ScrapingError::Parse("Failed to parse img selector".into()))?; - let ep_sel = selector(".epz") - .ok_or_else(|| ScrapingError::Parse("Failed to parse epz selector".into()))?; - for element in document.select(&venz_sel) { + for element in document.select(&card_sel) { let title = text_from_or(&element, &title_sel, ""); - let anime_url = attr_from_or(&element, &link_sel, "href", ""); + let anime_url = attr_from_or(&element, &title_sel, "href", ""); let slug = extract_slug(&anime_url); let poster = attr_from_or(&element, &img_sel, "src", ""); - let episode = text_from_or(&element, &ep_sel, "N/A"); + let episode = text_from_or(&element, &eps_sel, "N/A"); + let score = text_from_or(&element, &rating_sel, ""); if !title.is_empty() { items.push(GenreAnimeItem { @@ -544,7 +550,7 @@ pub fn parse_genre_anime_document( slug, poster, episode, - score: String::new(), + score, status: String::new(), anime_url, });