use std::sync::Arc; use crate::modules::anime::repository::AnimeRepository; use crate::modules::anime::types::*; use crate::shared::errors::AppError; use crate::shared::state::AppState; use crate::shared::utils::Cache; const INDEX_CACHE_TTL: u64 = 10; const GENRE_LIST_CACHE_TTL: u64 = 3600; const DEFAULT_CACHE_TTL: u64 = 300; pub struct AnimeService { repository: AnimeRepository, } impl AnimeService { pub fn new(repository: AnimeRepository) -> Self { Self { repository } } pub async fn get_anime_index(&self, app_state: Arc) -> Result { let cache = Cache::new(&app_state.redis_pool); cache .get_or_set("anime:index:v2", INDEX_CACHE_TTL, || async { let mut data = self .repository .fetch_anime_index() .await .map_err(|e| e.to_string())?; if data.ongoing_anime.is_empty() && data.complete_anime.is_empty() { return Err("Empty anime index — refusing to cache".to_string()); } let mut posters: Vec = data .ongoing_anime .iter() .map(|item| item.poster.clone()) .collect(); posters.extend(data.complete_anime.iter().map(|item| item.poster.clone())); let cached_posters = crate::shared::services::images::cache::cache_image_urls_batch_lazy( app_state.db.clone(), &app_state.redis_pool, posters, Some(app_state.image_processing_semaphore.clone()), ) .await; let ongoing_len = data.ongoing_anime.len(); for (i, item) in data.ongoing_anime.iter_mut().enumerate() { if let Some(url) = cached_posters.get(i) { item.poster = url.clone(); } } for (i, item) in data.complete_anime.iter_mut().enumerate() { if let Some(url) = cached_posters.get(ongoing_len + i) { item.poster = url.clone(); } } Ok(data) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_genres(&self, app_state: Arc) -> Result { let cache = Cache::new(&app_state.redis_pool); cache .get_or_set("anime:genres:list", GENRE_LIST_CACHE_TTL, || async { let genres = self .repository .fetch_genres() .await .map_err(|e| e.to_string())?; Ok(GenresResponse { status: "Ok".to_string(), data: genres, }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_anime_detail( &self, app_state: Arc, slug: String, ) -> Result { let cache_key = format!("anime:detail:{}", slug); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let mut data = self .repository .fetch_anime_detail(&slug) .await .map_err(|e| e.to_string())?; data.poster = crate::shared::services::images::cache::get_cached_or_original( app_state.db.clone(), &app_state.redis_pool, &data.poster, Some(app_state.image_processing_semaphore.clone()), ) .await; let rec_posters: Vec = data .recommendations .iter() .map(|r| r.poster.clone()) .collect(); let cached_rec_posters = crate::shared::services::images::cache::cache_image_urls_batch_lazy( app_state.db.clone(), &app_state.redis_pool, rec_posters, Some(app_state.image_processing_semaphore.clone()), ) .await; for (i, rec) in data.recommendations.iter_mut().enumerate() { if let Some(url) = cached_rec_posters.get(i) { rec.poster = url.clone(); } } Ok(DetailResponse { status: Some("Ok".to_string()), data, }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_complete_anime_page( &self, app_state: Arc, slug: String, ) -> Result { let cache_key = format!("anime:complete:{}", slug); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let (anime_list, pagination) = self .repository .fetch_complete_anime_page(&slug) .await .map_err(|e| e.to_string())?; let total = anime_list.len() as i64; Ok(ListResponse { message: "Success".to_string(), data: anime_list, total: Some(total), pagination: Some(pagination), }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_ongoing_anime_page( &self, app_state: Arc, slug: String, ) -> Result { let cache_key = format!("anime:ongoing:{}", slug); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let (anime_list, pagination) = self .repository .fetch_ongoing_anime_page(&slug) .await .map_err(|e| e.to_string())?; Ok(OngoingAnimeResponse { status: "Ok".to_string(), data: anime_list, pagination, }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_latest_anime_page( &self, app_state: Arc, slug: String, ) -> Result { let cache_key = format!("anime:latest:{}", slug); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let (anime_list, pagination) = self .repository .fetch_latest_anime_page(&slug) .await .map_err(|e| e.to_string())?; Ok(LatestAnimeResponse { status: "Ok".to_string(), data: anime_list, pagination, }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_search_anime_page( &self, app_state: Arc, slug: String, page: String, ) -> Result { let cache_key = format!("anime:search:{}:{}", slug, page); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let (anime_list, pagination) = self .repository .fetch_search_anime_page(&slug, &page) .await .map_err(|e| e.to_string())?; Ok(SearchResponse { status: "Ok".to_string(), data: anime_list, pagination, }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_genre_anime_page( &self, app_state: Arc, genre_slug: String, page: String, ) -> Result { let cache_key = format!("anime:genre:{}:{}", genre_slug, page); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let (anime_list, pagination) = self .repository .fetch_genre_anime_page(&genre_slug, &page) .await .map_err(|e| e.to_string())?; Ok(GenreListResponse { status: "Ok".to_string(), data: anime_list, pagination, }) }) .await .map_err(|e| AppError::ScraperError(e)) } pub async fn get_anime_full( &self, app_state: Arc, slug: String, ) -> Result { let cache_key = format!("anime:full:{}", slug); let cache = Cache::new(&app_state.redis_pool); cache .get_or_set(&cache_key, DEFAULT_CACHE_TTL, || async { let data = self .repository .fetch_anime_full(&slug) .await .map_err(|e| e.to_string())?; Ok(FullResponse { status: "Ok".to_string(), data, }) }) .await .map_err(|e| AppError::ScraperError(e)) } }