✨ 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>
26 KiB
26 KiB
Diagramas del Sistema de Descarga
1. Arquitectura General
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
├──────────────────────┬──────────────────┬───────────────────┤
│ MangaDetailView │ DownloadsView │ ReaderView │
│ - Download buttons │ - Active tab │ - Read offline │
│ - Progress bars │ - Completed tab │ - Use local URLs │
│ - Notifications │ - Failed tab │ │
└──────────┬───────────┴──────────┬───────┴─────────┬─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
├──────────────────────┬──────────────────┬───────────────────┤
│ MangaDetailViewModel│ DownloadsViewModel│ │
│ - downloadChapter() │ - clearAllStorage│ │
│ - downloadChapters()│ - showClearAlert│ │
└──────────┬──────────────────────────────┴───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Business Layer │
├─────────────────────────────────────────────────────────────┤
│ DownloadManager │
│ - downloadChapter() │
│ - downloadChapters() │
│ - cancelDownload() │
│ - cancelAllDownloads() │
│ - downloadImages() │
└──────────┬──────────────────────┬───────────────────────────┘
│ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────────────┐
│ Scraper Layer │ │ Storage Layer │
├────────────────────────┤ ├────────────────────────────────┤
│ ManhwaWebScraper │ │ StorageService │
│ - scrapeChapters() │ │ - saveImage() │
│ - scrapeChapterImages()│ │ - getImageURL() │
│ - scrapeMangaInfo() │ │ - isChapterDownloaded() │
│ │ │ - getChapterDirectory() │
│ │ │ - deleteDownloadedChapter() │
└────────────────────────┘ └────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Network Layer │
├─────────────────────────────────────────────────────────────┤
│ URLSession │
│ - downloadImage(from: URL) │
│ - data(from: URL) │
└─────────────────────────────────────────────────────────────┘
2. Flujo de Descarga Detallado
USUARIO TOCA "DESCARGAR CAPÍTULO"
│
▼
┌───────────────────────────────────────────┐
│ MangaDetailView │
│ Button tapped → downloadChapter() │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ MangaDetailViewModel │
│ 1. Verificar si ya está descargado │
│ 2. Llamar downloadManager.downloadChapter()│
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ DownloadManager │
│ 1. Verificar duplicados │
│ 2. Crear DownloadTask (state: .pending) │
│ 3. Agregar a activeDownloads[] │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ ManhwaWebScraper │
│ scrapeChapterImages(chapterSlug) │
│ → Retorna [String] URLs de imágenes │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ DownloadManager │
│ 4. Actualizar task.imageURLs │
│ 5. Iniciar downloadImages() │
│ Task 1: state = .downloading(0.0) │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ downloadImages() - CONCURRENCIA │
│ ┌─────────────────────────────────────┐ │
│ │ TaskGroup (max 5 concurrent) │ │
│ │ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │Img 0│ │Img 1│ │Img 2│ │Img 3│... │ │
│ │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ downloadImage(from: URL) │ │ │
│ │ │ 1. URLSession.data(from:) │ │ │
│ │ │ 2. Validar HTTP 200 │ │ │
│ │ │ 3. UIImage(data:) │ │ │
│ │ │ 4. optimizedForStorage() │ │ │
│ │ └───────────────┬───────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ StorageService.saveImage() │ │ │
│ │ │ → Documents/Chapters/... │ │ │
│ │ └───────────────┬───────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ task.updateProgress() │ │ │
│ │ │ downloadedPages += 1 │ │ │
│ │ │ progress = new value │ │ │
│ │ │ @Published → UI updates │ │ │
│ │ └───────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │
│ Repetir para todas las imágenes... │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ DownloadManager │
│ 6. Todas las imágenes descargadas │
│ 7. Crear DownloadedChapter metadata │
│ 8. storage.saveDownloadedChapter() │
│ 9. task.complete() → state = .completed │
│ 10. Mover de activeDownloads[] a │
│ completedDownloads[] │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ MangaDetailViewModel │
│ 11. showDownloadCompletionNotification() │
│ 12. "1 capítulo(s) descargado(s)" │
│ 13. loadChapters() para actualizar UI │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ UI ACTUALIZADA │
│ - ChapterRow muestra checkmark verde │
│ - Toast notification aparece │
│ - DownloadsView actualiza │
└───────────────────────────────────────────┘
3. Estados de una Descarga
┌─────────────────────────────────────────────────────────────┐
│ ESTADOS DE DESCARGA │
└─────────────────────────────────────────────────────────────┘
PENDING
┌──────────────────────┐
│ state: .pending │
│ downloadedPages: 0 │
│ progress: 0.0 │
│ UI: Icono gris │
└──────────┬───────────┘
│ Usuario inicia descarga
▼
DOWNLOADING
┌──────────────────────┐
│ state: .downloading │
│ downloadedPages: N │ ← Incrementando
│ progress: N/Total │ ← 0.0 a 1.0
│ UI: Barra azul │ ← Animando
└──────────┬───────────┘
│
├──────────────────┐
│ │
▼ ▼
COMPLETADO CANCELADO/ERROR
┌──────────────────┐ ┌──────────────────────┐
│ state: .completed│ │ state: .cancelled │
│ downloadedPages: │ │ state: .failed(error)│
│ Total │ │ downloadedPages: N │
│ progress: 1.0 │ │ progress: N/Total │
│ UI: Checkmark │ │ UI: X rojo / Icono │
└──────────────────┘ └──────────────────────┘
4. Cancelación de Descarga
USUARIO TOCA "CANCELAR"
│
▼
┌───────────────────────────────────────────┐
│ DownloadManager.cancelDownload(taskId) │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ 1. Encontrar task en activeDownloads[] │
│ 2. task.cancel() │
│ → cancellationToken.isCancelled = true│
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ TaskGroup detecta cancelación │
│ ┌─────────────────────────────────────┐ │
│ │ if task.isCancelled { │ │
│ │ throw DownloadError.cancelled │ │
│ │ } │ │
│ └─────────────────────────────────────┘ │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ downloadImages() lanza error │
│ → Catch block ejecuta cleanup │
└───────────────┬───────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ LIMPIEZA │
│ 1. Remover de activeDownloads[] │
│ 2. storage.deleteDownloadedChapter() │
│ → Eliminar imágenes parciales │
│ 3. NO agregar a completed[] │
└───────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ UI ACTUALIZADA │
│ - Progress bar desaparece │
│ - Icono de descarga restaurado │
└───────────────────────────────────────────┘
5. Concurrencia de Descargas
NIVEL 1: Descarga de Capítulos (max 3 simultáneos)
┌─────────────────────────────────────────────────────────────┐
│ downloadManager.downloadChapters([ch1, ch2, ch3, ch4...]) │
└───────────────┬─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TaskGroup (limitado a 3 tasks) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Chapter 1 │ │ Chapter 2 │ │ Chapter 3 │ ← Active │
│ │ downloading│ │ downloading│ │ downloading│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Chapter 4 │ │ Chapter 5 │ │ Chapter 6 │ ← Waiting │
│ │ waiting │ │ waiting │ │ waiting │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Cuando Chapter 1 completa → Chapter 4 inicia │
└─────────────────────────────────────────────────────────────┘
NIVEL 2: Descarga de Imágenes (max 5 simultáneas por capítulo)
┌─────────────────────────────────────────────────────────────┐
│ Chapter 1: downloadImages([img0, img1, ... img50]) │
└───────────────┬─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ TaskGroup (limitado a 5 tasks) │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │img0│ │img1│ │img2│ │img3│ │img4│ ← Descargando │
│ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ │
│ │ │ │ │ │ │
│ ┌─▼──────▼──────▼──────▼──────▼──┐ │
│ │ img5, img6, img7, img8, img9...│ ← Waiting │
│ └─────────────────────────────────┘ │
│ │
│ Cuando img0-4 completan → img5-9 inician │
└─────────────────────────────────────────────────────────────┘
RESULTADO: Máximo 15 imágenes descargando simultáneamente
(3 capítulos × 5 imágenes)
6. Gestión de Errores
┌─────────────────────────────────────────────────────────────┐
│ TIPOS DE ERROR │
└─────────────────────────────────────────────────────────────┘
NETWORK ERRORS
┌──────────────────────┐
│ - Timeout (30s) │ → Reintentar automáticamente
│ - No internet │ → Error al usuario
│ - HTTP 4xx, 5xx │ → Error específico
└──────────────────────┘
SCRAPER ERRORS
┌──────────────────────┐
│ - No images found │ → Error: "No se encontraron imágenes"
│ - Page load failed │ → Error: "Error al cargar página"
│ - Parsing error │ → Error: "Error al procesar"
└──────────────────────┘
STORAGE ERRORS
┌──────────────────────┐
│ - No space left │ → Error: "Espacio insuficiente"
│ - Permission denied │ → Error: "Sin permisos"
│ - Disk write error │ → Error: "Error de escritura"
└──────────────────────┘
VALIDATION ERRORS
┌──────────────────────┐
│ - Already downloaded │ → Skip o sobrescribir
│ - Invalid URL │ → Error: "URL inválida"
│ - Invalid image data │ → Error: "Imagen inválida"
└──────────────────────┘
7. Sincronización de UI
┌─────────────────────────────────────────────────────────────┐
│ @Published PROPERTIES │
└─────────────────────────────────────────────────────────────┘
DownloadManager
┌───────────────────────────────────┐
│ @Published var activeDownloads │ → Vista observa
│ @Published var completedDownloads │ → Vista observa
│ @Published var failedDownloads │ → Vista observa
│ @Published var totalProgress │ → Vista observa
└───────────────────────────────────┘
│
│ @Published cambia
▼
┌───────────────────────────────────┐
│ SwiftUI View se re-renderiza │
│ automáticamente │
└───────────────────────────────────┘
DownloadTask
┌───────────────────────────────────┐
│ @Published var state │ → Card observa
│ @Published var downloadedPages │ → ProgressView observa
│ @Published var progress │ → ProgressView observa
└───────────────────────────────────┘
│
│ @Published cambia
▼
┌───────────────────────────────────┐
│ ActiveDownloadCard se actualiza │
│ automáticamente │
└───────────────────────────────────┘
8. Estructura de Archivos
Documents/
└── Chapters/
└── {mangaSlug}/
└── Chapter{chapterNumber}/
├── page_0.jpg
├── page_1.jpg
├── page_2.jpg
├── ...
└── page_N.jpg
Ejemplo:
Documents/
└── Chapters/
└── one-piece_1695365223767/
└── Chapter1/
├── page_0.jpg (150 KB)
├── page_1.jpg (180 KB)
├── page_2.jpg (165 KB)
└── ...
└── Chapter2/
├── page_0.jpg
├── page_1.jpg
└── ...
metadata.json
{
"downloadedChapters": [
{
"id": "one-piece_1695365223767-chapter1",
"mangaSlug": "one-piece_1695365223767",
"mangaTitle": "One Piece",
"chapterNumber": 1,
"pages": [...],
"downloadedAt": "2026-02-04T10:30:00Z",
"totalSize": 5242880
}
]
}