feat: full clean architecture refactor — domain → application → infrastructure → presentation

- Restructure from Modular MVC to Clean Architecture with 4 strict layers
- Domain: entities (anime/komik/proxy), Repository traits, typed errors
- Application: use case classes with proper error propagation
- Infrastructure: repository impls, parsers, Redis cache, HTTP/scraping, browser
- Presentation: Axum handlers, DTOs, AppState, router, AppError+IntoResponse
- Migrate all parsers (otakudesu, alqanime, komik) to native infra implementations
- Replace once_cell::sync::Lazy/OnceCell with std::sync::LazyLock/OnceLock
- Remove 150+ old files in modules/ and shared/ directories
- Remove once_cell from Cargo.toml dependencies
- Fix test/debug binaries to use new import paths
- Zero new clippy warnings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
asepharyana
2026-07-21 12:54:23 +07:00
co-authored by Claude Opus 4.8
parent 80c96eaa42
commit 776fa828d8
205 changed files with 4241 additions and 14569 deletions
+19 -10
View File
@@ -1,3 +1,5 @@
pub mod setup;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
@@ -6,9 +8,9 @@ use axum::Router;
use sea_orm::Database;
use tracing_subscriber::EnvFilter;
use crate::shared::config::CONFIG;
use crate::shared::database::get_redis_conn;
use crate::shared::state::AppState;
use crate::config::CONFIG;
use crate::infrastructure::cache::redis_pool::get_redis_conn;
use crate::presentation::state::AppState;
pub struct Application {
pub port: u16,
@@ -27,7 +29,7 @@ impl Application {
tracing_subscriber::fmt().with_env_filter(env_filter).init();
// Initialize OpenTelemetry metrics
crate::shared::observability::metrics::init_otel_metrics();
crate::observability::metrics::init_otel_metrics();
tracing::info!("🚀 Scraper starting up...");
tracing::info!(" Environment: {}", CONFIG.environment);
@@ -46,8 +48,8 @@ impl Application {
// Browser Pool
tracing::info!("Initializing browser pool...");
let browser_config = crate::shared::browser::BrowserPoolConfig::default();
match crate::shared::browser::pool::init_browser_pool(browser_config).await {
let browser_config = crate::infrastructure::browser::BrowserPoolConfig::default();
match crate::infrastructure::browser::pool::init_browser_pool(browser_config).await {
Ok(_) => tracing::info!("✓ Browser pool initialized"),
Err(e) => tracing::error!("⚠️ Failed to initialize browser pool: {}", e),
}
@@ -76,7 +78,7 @@ impl Application {
tracing::info!("✓ SeaORM database connection established");
// Schema & Seeding
if let Err(e) = crate::shared::database::setup::init(&db).await {
if let Err(e) = crate::bootstrap::setup::init(&db).await {
tracing::error!("Failed to init DB schema: {}", e);
}
@@ -85,19 +87,26 @@ impl Application {
let image_processing_semaphore = Arc::new(tokio::sync::Semaphore::new(
CONFIG.image_processing_concurrency,
));
let event_bus = Arc::new(crate::shared::events::bus::EventBus::new());
let event_bus = Arc::new(crate::events::bus::EventBus::new());
let redis_pool = crate::shared::database::redis_pool()
let redis_pool = crate::infrastructure::cache::redis_pool::redis_pool()
.map_err(|e| anyhow::anyhow!("Failed to init Redis pool: {}", e))?;
use crate::infrastructure::repository::SeaOrmImageCacheRepository;
let image_cache_repo = Arc::new(SeaOrmImageCacheRepository::new(
db_arc.clone(),
redis_pool.clone(),
));
let app_state = Arc::new(AppState {
redis_pool,
db: db_arc.clone(),
image_processing_semaphore,
event_bus: event_bus.clone(),
image_cache_repo,
});
let app = crate::app::build_router(app_state, db_arc.clone()).await?;
let app = crate::presentation::router::build_router(app_state.clone())?;
// Listener
let port = CONFIG.server_port;
+47
View File
@@ -0,0 +1,47 @@
//! Database schema initialization.
use sea_orm::{ConnectionTrait, DatabaseConnection, Schema, Statement};
use tracing::info;
use crate::infrastructure::persistence::entities::image_cache;
pub async fn init(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {
info!("🚀 Initializing database schema...");
let backend = db.get_database_backend();
let schema = Schema::new(backend);
let tables = vec![(
"ImageCache",
schema
.create_table_from_entity(image_cache::Entity)
.if_not_exists()
.to_owned(),
)];
for (name, stmt) in tables {
match db.execute(backend.build(&stmt)).await {
Ok(_) => info!(" ✓ Table '{}' checked/created", name),
Err(e) => {
tracing::error!(" [!] Failed to create table '{}': {}", name, e);
return Err(e);
}
}
}
let index_sql =
"CREATE INDEX IF NOT EXISTS idx_image_cache_cdn_url ON \"ImageCache\" (cdn_url)";
match db.execute(Statement::from_string(backend, index_sql)).await {
Ok(_) => info!(" ✓ Index 'idx_image_cache_cdn_url' ensured"),
Err(e) => {
let err_str = e.to_string();
if err_str.contains("already exists") || err_str.contains("duplicate") {
info!(" ✓ Index 'idx_image_cache_cdn_url' already exists");
} else {
tracing::error!(" [!] Failed to create index on ImageCache: {}", e);
}
}
}
info!("✅ Database schema initialization complete.");
Ok(())
}