Files
MangaReader/docs/ARCHITECTURE.md
renato97 b474182dd9 Initial commit: MangaReader iOS App
 Features:
- App iOS completa para leer manga sin publicidad
- Scraper con WKWebView para manhwaweb.com
- Sistema de descargas offline
- Lector con zoom y navegación
- Favoritos y progreso de lectura
- Compatible con iOS 15+ y Sideloadly/3uTools

📦 Contenido:
- Backend Node.js con Puppeteer (opcional)
- App iOS con SwiftUI
- Scraper de capítulos e imágenes
- Sistema de almacenamiento local
- Testing completo
- Documentación exhaustiva

🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente
  - 21 páginas descargadas
  - 4.68 MB total
  - URLs verificadas y funcionales

🎉 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-04 15:34:18 +01:00

643 lines
32 KiB
Markdown

# Arquitectura de MangaReader
Este documento describe la arquitectura general del proyecto MangaReader, explicando cómo funcionan los componentes Backend y iOS App, y cómo fluyen los datos desde el scraping hasta el display en pantalla.
## Tabla de Contenidos
- [Visión General](#visión-general)
- [Arquitectura del Sistema](#arquitectura-del-sistema)
- [Arquitectura de la App iOS](#arquitectura-de-la-app-ios)
- [Flujo de Datos](#flujo-de-datos)
- [Patrones de Diseño](#patrones-de-diseño)
- [Diagramas de Secuencia](#diagramas-de-secuencia)
## Visión General
MangaReader es una aplicación nativa de iOS para leer manga sin publicidad. El proyecto consta de dos componentes opcionales:
1. **Backend (Opcional)**: Servidor Node.js con Express que realiza scraping usando Puppeteer
2. **iOS App**: Aplicación nativa SwiftUI que puede hacer scraping localmente usando WKWebView
```
┌─────────────────────────────────────────────────────────────────┐
│ MangaReader │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Backend │ │ iOS App │ │
│ │ (Opcional) │ │ (Principal) │ │
│ │ │ │ │ │
│ │ • Node.js │ │ • SwiftUI │ │
│ │ • Express │ │ • WKWebView │ │
│ │ • Puppeteer │ │ • Core Data │ │
│ └─────────────────┘ └─────────────────┘ │
│ ▲ │ │
│ │ │ │
│ └──────────────────────────────────┘ │
│ Scraping Independiente │
└─────────────────────────────────────────────────────────────────┘
```
## Arquitectura del Sistema
### Componentes Principales
#### 1. Backend (Opcional)
El backend es una API REST opcional que puede actuar como intermediario:
```
backend/
├── scraper.js # Scraper con Puppeteer
├── server.js # API REST con Express
└── package.json
```
**Responsabilidades:**
- Realizar scraping de manhwaweb.com
- Servir datos vía API REST
- Cachear respuestas para mejorar rendimiento
**API Endpoints:**
- `GET /api/health` - Health check
- `GET /api/manga/:slug` - Información de un manga
- `GET /api/manga/:slug/chapters` - Lista de capítulos
- `GET /api/chapter/:slug/images` - Imágenes de un capítulo
**Nota Importante**: El backend es completamente opcional. La app iOS está diseñada para funcionar de manera autónoma sin necesidad del backend.
#### 2. iOS App (Principal)
La aplicación iOS es el componente principal y puede operar independientemente:
```
ios-app/
├── MangaReaderApp.swift # Entry point
├── Info.plist
└── Sources/
├── Models/ # Modelos de datos
│ └── Manga.swift
├── Services/ # Lógica de negocio
│ ├── ManhwaWebScraper.swift
│ └── StorageService.swift
└── Views/ # UI SwiftUI
├── ContentView.swift
├── MangaDetailView.swift
└── ReaderView.swift
```
## Arquitectura de la App iOS
### MVVM Pattern (Model-View-ViewModel)
La app iOS sigue el patrón MVVM para separar la UI de la lógica de negocio:
```
┌─────────────────────────────────────────────────────────────────┐
│ MVVM Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────────────┐ ┌─────────┐ │
│ │ View │◄────────│ ViewModel │◄────────│ Model │ │
│ │(SwiftUI)│ │ (Observable) │ │(Struct) │ │
│ └─────────┘ └─────────────────┘ └─────────┘ │
│ ▲ │ │ │
│ │ │ │ │
│ └───────────────────────┴───────────────────────────┘ │
│ Data Binding & Commands │
└─────────────────────────────────────────────────────────────────┘
```
### Estructura de Componentes
#### 1. Models (Datos)
**Ubicación**: `ios-app/Sources/Models/Manga.swift`
Los modelos son estructuras inmutables que representan los datos:
```swift
- Manga: Información del manga (título, descripción, géneros, estado)
- Chapter: Capítulo individual (número, título, URL, estado de lectura)
- MangaPage: Página individual del capítulo (URL de imagen, índice)
- ReadingProgress: Progreso de lectura del usuario
- DownloadedChapter: Capítulo descargado localmente
```
**Características:**
- Inmutables (`struct`)
- Conformes a `Codable` para serialización
- Conformes a `Identifiable` para SwiftUI
- Conformes a `Hashable` para comparaciones
#### 2. Services (Lógica de Negocio)
**Ubicación**: `ios-app/Sources/Services/`
##### ManhwaWebScraper.swift
Responsable del scraping de contenido web:
```swift
class ManhwaWebScraper: NSObject, ObservableObject {
// Singleton instance
static let shared = ManhwaWebScraper()
// Funciones principales:
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
func scrapeChapterImages(chapterSlug: String) async throws -> [String]
}
```
**Características:**
- Usa `WKWebView` para ejecutar JavaScript
- Implementa `async/await` para operaciones asíncronas
- Patrón Singleton para compartir instancia
- Manejo de errores con `ScrapingError`
##### StorageService.swift
Responsable del almacenamiento local:
```swift
class StorageService {
// Singleton instance
static let shared = StorageService()
// Gestión de favoritos:
func getFavorites() -> [String]
func saveFavorite(mangaSlug: String)
func removeFavorite(mangaSlug: String)
// Gestión de progreso:
func saveReadingProgress(_ progress: ReadingProgress)
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?
// Gestión de descargas:
func saveDownloadedChapter(_ chapter: DownloadedChapter)
func getDownloadedChapters() -> [DownloadedChapter]
// Gestión de imágenes:
func saveImage(_ image: UIImage, ...) async throws -> URL
func loadImage(...) -> UIImage?
}
```
**Características:**
- Almacena favoritos en `UserDefaults`
- Almacena progreso en `UserDefaults`
- Guarda imágenes en el sistema de archivos
- Usa `FileManager` para gestión de archivos
#### 3. ViewModels (Presentación)
**Ubicación**: Integrados en los archivos de Views
Los ViewModels coordinan entre Services y Views:
```swift
@MainActor
class MangaListViewModel: ObservableObject {
@Published var mangas: [Manga] = []
@Published var isLoading = false
private let scraper = ManhwaWebScraper.shared
private let storage = StorageService.shared
func loadMangas() async
func addManga(_ slug: String) async
}
```
**Responsabilidades:**
- Mantener estado de la UI
- Transformar datos para presentación
- Manejar lógica de navegación
- Coordinar llamadas a servicios
#### 4. Views (UI)
**Ubicación**: `ios-app/Sources/Views/`
##### ContentView.swift
- Vista principal de la app
- Lista de mangas con filtros
- Búsqueda y añadir manga
##### MangaDetailView.swift
- Detalle de un manga específico
- Lista de capítulos
- Descarga de capítulos
##### ReaderView.swift
- Lector de imágenes
- Gestos de zoom y pan
- Configuración de lectura
## Flujo de Datos
### 1. Flujo de Scraping de Manga
```
Usuario ingresa slug
┌───────────────────────────────────────────────────────────────┐
│ ContentView -> MangaListViewModel.addManga(slug) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ManhwaWebScraper.scrapeMangaInfo(mangaSlug) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ WKWebView carga URL de manhwaweb.com │
│ https://manhwaweb.com/manga/{slug} │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ JavaScript ejecutado en WKWebView: │
│ - Extrae título, descripción, géneros │
│ - Extrae estado, imagen de portada │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Datos parseados a struct Manga │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ViewModel actualiza @Published var mangas │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ SwiftUI detecta cambio y re-renderiza UI │
└───────────────────────────────────────────────────────────────┘
```
### 2. Flujo de Lectura de Capítulo
```
Usuario selecciona capítulo
┌───────────────────────────────────────────────────────────────┐
│ MangaDetailView -> ReaderView(manga, chapter) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ReaderViewModel.loadPages() │
└───────────────────────────────────────────────────────────────┘
├──► ¿Capítulo descargado?
│ │
│ SÍ │ NO
│ ▼
│ ┌─────────────────────────────────────────────────┐
│ │ StorageService.getDownloadedChapter() │
│ │ Cargar páginas locales │
│ └─────────────────────────────────────────────────┘
│ │
│ └──────────────────┐
│ │
└─────────────────────────────┼──────┐
│ │
▼ ▼
┌───────────────────────────────────────────────────────────────┐
│ ManhwaWebScraper.scrapeChapterImages(chapterSlug) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ WKWebView carga URL del capítulo │
│ https://manhwaweb.com/leer/{slug} │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ JavaScript extrae URLs de imágenes: │
│ - Selecciona todas las etiquetas <img> │
│ - Filtra elementos de UI (avatars, icons) │
│ - Elimina duplicados │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Array de strings con URLs de imágenes │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Convertir a [MangaPage] y mostrar en ReaderView │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ReaderView muestra imágenes con AsyncImage │
│ Cache automático de imágenes en visualización │
└───────────────────────────────────────────────────────────────┘
```
### 3. Flujo de Guardado de Progreso
```
Usuario navega a página X
┌───────────────────────────────────────────────────────────────┐
│ ReaderViewModel.currentPage cambia a X │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ReaderViewModel.saveProgress() │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Crear ReadingProgress(mangaSlug, chapterNumber, pageNumber) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ StorageService.saveReadingProgress(progress) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ JSONEncoder codifica a Data │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ UserDefaults.set(data, forKey: "readingProgress") │
└───────────────────────────────────────────────────────────────┘
```
## Patrones de Diseño
### 1. Singleton Pattern
**Uso**: Services compartidos
```swift
class StorageService {
static let shared = StorageService()
private init() { ... }
}
class ManhwaWebScraper {
static let shared = ManhwaWebScraper()
private init() { ... }
}
```
**Beneficios:**
- Unica instancia compartida en toda la app
- Reduce consumo de memoria
- Facilita acceso desde cualquier View/ViewModel
### 2. MVVM (Model-View-ViewModel)
**Uso**: Arquitectura general de la app
**Separación de responsabilidades:**
- **Model**: Datos puras (`struct`, `Codable`)
- **View**: UI pura (`SwiftUI`, reactive)
- **ViewModel**: Lógica de presentación (`ObservableObject`)
**Beneficios:**
- Testabilidad de ViewModels sin UI
- Reutilización de ViewModels
- Separación clara de concerns
### 3. Repository Pattern
**Uso**: Abstracción de fuentes de datos
```swift
class StorageService {
// Abstrae UserDefaults, FileManager, etc.
func getFavorites() -> [String]
func saveFavorite(mangaSlug: String)
}
```
**Beneficios:**
- Interfaz unificada para diferentes storage
- Fácil cambiar implementación
- Centraliza lógica de persistencia
### 4. Async/Await Pattern
**Uso**: Operaciones de scraping
```swift
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga {
// Operación asíncrona
try await loadURLAndWait(url)
let info = try await webView.evaluateJavaScript(...)
return Manga(...)
}
```
**Beneficios:**
- Código asíncrono legible
- Manejo de errores claro
- No bloquea el hilo principal
### 5. Observable Object Pattern
**Uso**: Reactividad en SwiftUI
```swift
@MainActor
class MangaListViewModel: ObservableObject {
@Published var mangas: [Manga] = []
func loadMangas() async {
mangas = ... // SwiftUI detecta cambio
}
}
```
**Beneficios:**
- UI se actualiza automáticamente
- Código declarativo
- Menos código boilerplate
### 6. Factory Pattern (Implícito)
**Uso**: Creación de modelos
```swift
// Funciones estáticas que crean instancias
Chapter(number: 1, title: "...", url: "...", slug: "...")
MangaPage(url: "...", index: 0)
```
**Beneficios:**
- Creación consistente de objetos
- Validación en inicialización
- Fácil de mantener
## Diagramas de Secuencia
### Secuencia 1: Agregar Manga
```
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌────────────┐
│ Usuario │ │ ViewModel │ │ Scraper │ │ WKWebView │
└────┬────┘ └──────┬───────┘ └────────┬────────┘ └─────┬──────┘
│ │ │ │
│ ingresa slug │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ scrapeMangaInfo() │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ load(URL) │
│ │ │───────────────────>│
│ │ │ │
│ │ │ wait 3 seconds │
│ │ │<───────────────────┤
│ │ │ │
│ │ │ evaluateJavaScript │
│ │ │───────────────────>│
│ │ │ (extrae datos) │
│ │ │<───────────────────┤
│ │ │ │
│ │ Manga │ │
│ │<────────────────────│ │
│ │ │ │
│ actualiza │ │ │
│ UI │ │ │
│<───────────────│ │ │
│ │ │ │
┌────┴────┐ ┌──────┴───────┘ └────────┴────────┘ └─────┴──────┘
```
### Secuencia 2: Leer Capítulo
```
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────┐
│ Usuario │ │ ReaderView │ │ ViewModel │ │ Storage │
└────┬────┘ └──────┬───────┘ └────────┬────────┘ └────┬─────┘
│ │ │ │
│ tap capítulo │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ loadPages() │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ isDownloaded()? │
│ │ │──────────────────>│
│ │ │ │
│ │ │ NO │
│ │ │<──────────────────┤
│ │ │ │
│ │ │ scrapeChapterImages│
│ │ │ (via Scraper) │
│ │ │ │
│ │ [MangaPage] │ │
│ │<────────────────────│ │
│ │ │ │
│ muestra páginas│ │ │
│<───────────────│ │ │
│ │ │ │
┌────┴────┐ ┌──────┴───────┘ └────────┴────────┘ └────┴─────┘
```
### Secuencia 3: Guardar Favorito
```
┌─────────┐ ┌──────────────┐ ┌─────────────────┐
│ Usuario │ │ View │ │ StorageService │
└────┬────┘ └──────┬───────┘ └────────┬────────┘
│ │ │
│ tap corazón │ │
│───────────────>│ │
│ │ │
│ │ toggleFavorite() │
│ │────────────────────>│
│ │ │
│ │ │ getFavorites()
│ │ │ (UserDefaults)
│ │ │
│ │ │ saveFavorite()
│ │ │ (UserDefaults)
│ │ │
│ │ actualiza UI │
│ │<────────────────────│
│ │ │
┌────┴────┘ ┌──────┴───────┘ └────────┴────────┘
```
## Decisiones de Arquitectura
### ¿Por qué WKWebView para scraping?
1. **JavaScript Rendering**: manhwaweb.com usa JavaScript para cargar contenido
2. **Sin dependencias externas**: No requiere librerías de terceros
3. **Aislamiento**: El scraping ocurre en contexto separado
4. **Control**: Full control sobre timeouts, cookies, headers
### ¿Por qué UserDefaults para favoritos/progreso?
1. **Simplicidad**: Datos pequeños y simples
2. **Sincronización**: iCloud sync automático disponible
3. **Rendimiento**: Lectura/escritura rápida
4. **Persistencia**: Survive app reinstalls (si iCloud)
### ¿Por qué FileManager para imágenes?
1. **Tamaño**: Imágenes pueden ser grandes (MBs)
2. **Performance**: Acceso directo a archivos
3. **Cache control**: Control manual de qué guardar
4. **Escalabilidad**: No limitado por UserDefaults
### ¿Por qué MVVM?
1. **SwiftUI nativo**: SwiftUI está diseñado para MVVM
2. **Testabilidad**: ViewModels testeables sin UI
3. **Reactibilidad**: `@Published` y `ObservableObject`
4. **Separación**: UI separada de lógica de negocio
## Consideraciones de Escalabilidad
### Futuras Mejoras
1. **Database**: Migrar de UserDefaults a Core Data o SQLite
2. **Background Tasks**: Descargas en background
3. **Caching Strategy**: LRU cache para imágenes
4. **Pagination**: Cargar capítulos bajo demanda
5. **Sync Service**: Sincronización entre dispositivos
### Rendimiento
- **Lazy Loading**: Cargar imágenes bajo demanda
- **Image Compression**: JPEG 80% calidad
- **Request Batching**: Descargar páginas en paralelo
- **Memory Management**: Liberar imágenes no visibles
## Seguridad
### Consideraciones
1. **No se almacenan credenciales**: La app no requiere login
2. **SSL Pinning**: Considerar para producción
3. **Input Validation**: Validar slugs antes de scraping
4. **Rate Limiting**: No sobrecargar el servidor objetivo
---
**Última actualización**: Febrero 2026
**Versión**: 1.0.0