# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Scraper service — a Rust/Axum backend for web scraping (anime/komik data extraction) and image proxy/CDN caching. Serves as the backend engine consumed by the `apps/solidjs` frontend. ## Commands ```bash # Development cargo run # Start server (binds 0.0.0.0:4091) cargo test # Run all tests cargo clippy -- -D warnings # Lint (warnings are errors) cargo fmt # Auto-format all source files # Release build (full LTO, single CGU, stripped) cargo build --release # PM2 production pm2 start ecosystem.config.cjs --env production # Uses target/release/scraper ``` ## Architecture ### Modular MVC + Service + Repository Setiap module mengikuti arsitektur layered yang identik: ``` Request → Router (route.rs) → Controller → Service → Repository → Parser │ ├── Redis (L1 cache) ├── SeaORM/MySQL (L2, image_cache) └── External HTTP (alqanime.net, picser CDN) ``` ### Directory Layout ``` src/ ├── main.rs # Entry point: builds Application, calls run() ├── lib.rs # Public module declarations ├── app.rs # Router assembly: modules + metrics + swagger + middleware layers ├── bootstrap/mod.rs # Application::build(): tracing, Redis, browser pool, DB, AppState ├── modules/ # Feature modules (vertical slices) │ ├── anime/ # Otakudesu anime scraper │ ├── anime2/ # Alqanime.si anime scraper │ ├── komik/ # Komik scraper │ └── proxy/ # Image proxy/cache/audit endpoints └── shared/ # Cross-cutting infrastructure ├── config/ # Lazy-static AppConfig from env vars (fail-fast at startup) ├── state/ # AppState (redis_pool, db, semaphore, event_bus) ├── database/ │ ├── traits/ # ScrapingRepository, ImageCacheRepository (async_trait) │ ├── repositories/ # SeaOrmImageCacheRepository (impl ImageCacheRepository) │ └── persistence/ # SeaORM entities (image_cache) ├── services/images/ # ImageCache service + apply_cached_posters helper ├── errors/ # AppError enum → axum IntoResponse (500/404 by variant) ├── observability/ # Utoipa/Swagger OpenAPI doc ├── scheduler/ # Cron jobs (daily cache cleanup at 2 AM) ├── browser/ # Headless Chrome pool for JS-rendered scraping ├── scrapers/ # Site-specific scrapers (otakudesu) ├── utils/ # Cache helper, HTTP client, scraping helpers, retry, conversions ├── middlewares/ # Logging, rate limiting ├── events/ # EventBus for repair state updates └── types/ # ApiResponse, shared entity types (HasPoster trait, Pagination) ``` ### Module Structure (identik untuk setiap module) Setiap `src/modules//`: | File | Peran | Pola | |---|---|---| | `route.rs` | Daftar endpoint, mapping URL → controller | `Router>`, tidak ada logic | | `controller.rs` | Extract State/Path/Query/Body, panggil service | `Result, AppError>` | | `service.rs` | Business logic, caching, delegasi ke repository + parser | Struct dengan repo di-inject via constructor `new(repo: XRepository)` | | `repository.rs` | HTTP fetching, URL builders, DB queries | Struct + `impl ScrapingRepository` trait | | `parser.rs` | HTML parsing dengan `scraper` crate | Free functions → `Result`, via `spawn_blocking` | | `schema.rs` | Validasi query/path/body params | Struct `Deserialize` + `ToSchema` | | `types.rs` | Response structs | `Serialize` + `ToSchema`, `impl HasPoster` jika punya poster | ### Dependency Injection Semua service menerima dependency via constructor: ```rust // Controller creates and injects dependencies let repo = AnimeRepository::new(); let service = AnimeService::new(repo); service.get_anime_index(app_state).await.map(Json) // Service stores injected repo pub struct AnimeService { repository: AnimeRepository, } impl AnimeService { pub fn new(repository: AnimeRepository) -> Self { Self { repository } } } ``` ### Image Caching Architecture Single unified image cache system: 1. **Trait**: `ImageCacheRepository` (`shared/database/traits/image_cache.rs`) — Redis ops, DB ops, locks, cache invalidation 2. **Impl**: `SeaOrmImageCacheRepository` (`shared/database/repositories/image_cache.rs`) 3. **Service**: `ImageCache` struct (`shared/services/images/cache.rs`) — download, MIME-verify dengan `infer`, upload ke Picser CDN, verifikasi CDN URL (10 retry dengan backoff) 4. **Concurrency**: `Semaphore` (default 5 concurrent uploads) + request coalescing via `DashMap` 5. **Lazy batch helper**: `cache_image_urls_batch_lazy()` — Redis batch check → DB batch check → background spawn untuk misses ### HasPoster Trait & apply_cached_posters `HasPoster` trait di `shared/types/entities/anime.rs` memungkinkan generic poster caching: ```rust pub trait HasPoster { fn poster(&self) -> &str; fn set_poster(&mut self, url: String); } ``` Semua item type dengan field `poster` mengimplementasikan trait ini (`OngoingAnimeItem`, `KomikItem`, `FilterAnimeItem`, `Recommendation`, dll). `apply_cached_posters()` di `shared/services/images/cache.rs` menerima `&mut [T]` where `T: HasPoster`, menggantikan pola manual ~15 baris yang sebelumnya berulang di setiap service method. ### ScrapingRepository Trait ```rust #[async_trait] pub trait ScrapingRepository: Send + Sync { async fn fetch_html(&self, url: &str) -> Result; } ``` Semua module repository (`AnimeRepository`, `Anime2Repository`, `KomikRepository`, `ProxyRepository`) mengimplementasikan trait ini. ### Error Handling `AppError` enum di `src/shared/errors/app_error.rs` — derives `thiserror::Error` dan implements `IntoResponse` (404 untuk `NotFound`, 500 untuk lainnya). **Kontrak error per layer:** - **Parser** → `Result` - **Repository** → `Result` (via `ScrapingRepository` trait) - **Service** → `Result` (tidak ada `Result` atau `Box`) - **Controller** → `Result, AppError>` (kecuali proxy yang return raw `Response`) ### Configuration `src/shared/config/mod.rs` — global `CONFIG` lazy-static loaded from: 1. `.env` file (dotenvy) 2. `config/default.toml` / `config/{RUN_MODE}.toml` 3. Environment variables (`APP__` prefix or legacy `DATABASE_URL`/`JWT_SECRET`/`REDIS_URL`) Panics at startup if required config is missing — intentional fail-fast design. ## Constraints - **No suppression flags**: `#[allow(...)]`, `#[ignore]`, `@ts-ignore` are prohibited. Fix the underlying issue. - **Lint strictness**: `unsafe_code = "forbid"`, `panic = "deny"`, `todo = "deny"`, `unimplemented = "deny"`, `unwrap_used = "warn"`, `expect_used = "warn"` - **Minimal dependencies**: Before adding a crate, evaluate if existing deps or std can handle it. - **Dead code**: Remove unused functions, types, modules rather than leaving them. - **Performance**: Use `spawn_blocking` for CPU-heavy work (HTML parsing). - **No duplicate infrastructure**: Satu trait, satu impl. Jangan membuat trait/repository duplikat seperti `ImageRepository` dan `ImageCacheRepository` yang berbeda. - **No thin wrappers**: Hindari wrapper tipis seperti `CacheImageUseCase` yang hanya meneruskan panggilan ke service lain. ## Useful Endpoints - `GET /docs` — Swagger UI - `GET /api-docs/openapi.json` — OpenAPI spec - `POST /api/proxy/image-cache` — Cache an image URL - `POST /api/proxy/image-cache/audit` — Audit/repair cached images - `GET /api/anime/*` — Otakudesu scraping endpoints - `GET /api/anime2/*` — Alqanime scraping endpoints - `GET /api/komik/*` — Komik scraping endpoints