✨ 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>
344 lines
9.1 KiB
Markdown
344 lines
9.1 KiB
Markdown
# Sistema de Descarga de Capítulos - MangaReader iOS
|
|
|
|
## Overview
|
|
|
|
El sistema de descarga de capítulos permite a los usuarios descargar capítulos completos de manga para lectura offline. El sistema está diseñado con arquitectura asíncrona moderna usando Swift async/await.
|
|
|
|
## Componentes Principales
|
|
|
|
### 1. DownloadManager (`/Sources/Services/DownloadManager.swift`)
|
|
|
|
Gerente centralizado que maneja todas las operaciones de descarga.
|
|
|
|
**Características:**
|
|
- Descarga asíncrona de imágenes con concurrencia controlada
|
|
- Máximo 3 descargas simultáneas de capítulos
|
|
- Máximo 5 imágenes simultáneas por capítulo
|
|
- Cancelación de descargas individuales o masivas
|
|
- Seguimiento de progreso en tiempo real
|
|
- Manejo robusto de errores
|
|
- Historial de descargas completadas y fallidas
|
|
|
|
**Uso básico:**
|
|
```swift
|
|
let downloadManager = DownloadManager.shared
|
|
|
|
// Descargar un capítulo
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: "one-piece",
|
|
mangaTitle: "One Piece",
|
|
chapter: chapter
|
|
)
|
|
|
|
// Descargar múltiples capítulos
|
|
await downloadManager.downloadChapters(
|
|
mangaSlug: "one-piece",
|
|
mangaTitle: "One Piece",
|
|
chapters: chapters
|
|
)
|
|
|
|
// Cancelar descarga
|
|
downloadManager.cancelDownload(taskId: "taskId")
|
|
|
|
// Cancelar todas
|
|
downloadManager.cancelAllDownloads()
|
|
```
|
|
|
|
### 2. MangaDetailView (`/Sources/Views/MangaDetailView.swift`)
|
|
|
|
Vista de detalles del manga con funcionalidad de descarga integrada.
|
|
|
|
**Características añadidas:**
|
|
- Botón de descarga en la toolbar
|
|
- Descarga individual por capítulo
|
|
- Progreso de descarga visible en cada fila de capítulo
|
|
- Notificaciones de completado/error
|
|
- Alert para descargar últimos 10 o todos los capítulos
|
|
|
|
**Flujo de descarga:**
|
|
1. Usuario toca botón de descarga en toolbar → muestra alert
|
|
2. Selecciona cantidad de capítulos a descargar
|
|
3. Cada capítulo muestra progreso de descarga en tiempo real
|
|
4. Notificación aparece al completar todas las descargas
|
|
5. Capítulos descargados muestran checkmark verde
|
|
|
|
### 3. DownloadsView (`/Sources/Views/DownloadsView.swift`)
|
|
|
|
Vista dedicada para gestionar todas las descargas.
|
|
|
|
**Tabs:**
|
|
- **Activas**: Descargas en progreso con barra de progreso
|
|
- **Completadas**: Historial de descargas exitosas
|
|
- **Fallidas**: Descargas con errores, permite reintentar
|
|
|
|
**Funcionalidades:**
|
|
- Cancelar descargas individuales
|
|
- Cancelar todas las descargas activas
|
|
- Limpiar historiales (completadas/fallidas)
|
|
- Ver tamaño de almacenamiento usado
|
|
- Limpiar todo el almacenamiento descargado
|
|
|
|
### 4. StorageService (`/Sources/Services/StorageService.swift`)
|
|
|
|
Servicio de almacenamiento ya existente, ahora con soporte para descargas.
|
|
|
|
**Métodos utilizados:**
|
|
```swift
|
|
// Guardar imagen descargada
|
|
try await storage.saveImage(
|
|
image,
|
|
mangaSlug: "manga-slug",
|
|
chapterNumber: 1,
|
|
pageIndex: 0
|
|
)
|
|
|
|
// Verificar si capítulo está descargado
|
|
storage.isChapterDownloaded(mangaSlug: "manga-slug", chapterNumber: 1)
|
|
|
|
// Obtener directorio del capítulo
|
|
let chapterDir = storage.getChapterDirectory(
|
|
mangaSlug: "manga-slug",
|
|
chapterNumber: 1
|
|
)
|
|
|
|
// Obtener URL de imagen local
|
|
if let imageURL = storage.getImageURL(
|
|
mangaSlug: "manga-slug",
|
|
chapterNumber: 1,
|
|
pageIndex: 0
|
|
) {
|
|
// Usar imagen local
|
|
}
|
|
|
|
// Eliminar capítulo descargado
|
|
storage.deleteDownloadedChapter(
|
|
mangaSlug: "manga-slug",
|
|
chapterNumber: 1
|
|
)
|
|
|
|
// Obtener tamaño de almacenamiento
|
|
let size = storage.getStorageSize()
|
|
let formatted = storage.formatFileSize(size)
|
|
```
|
|
|
|
## Modelos de Datos
|
|
|
|
### DownloadTask
|
|
Representa una tarea de descarga individual:
|
|
```swift
|
|
class DownloadTask: ObservableObject {
|
|
let id: String
|
|
let mangaSlug: String
|
|
let mangaTitle: String
|
|
let chapterNumber: Int
|
|
let imageURLs: [String]
|
|
|
|
@Published var state: DownloadState
|
|
@Published var downloadedPages: Int
|
|
@Published var progress: Double
|
|
}
|
|
```
|
|
|
|
### DownloadState
|
|
Estados posibles de una descarga:
|
|
```swift
|
|
enum DownloadState {
|
|
case pending
|
|
case downloading(progress: Double)
|
|
case completed
|
|
case failed(error: String)
|
|
case cancelled
|
|
}
|
|
```
|
|
|
|
### DownloadError
|
|
Tipos de errores de descarga:
|
|
```swift
|
|
enum DownloadError: LocalizedError {
|
|
case alreadyDownloaded
|
|
case noImagesFound
|
|
case invalidURL
|
|
case invalidResponse
|
|
case httpError(statusCode: Int)
|
|
case invalidImageData
|
|
case cancelled
|
|
case storageError(String)
|
|
}
|
|
```
|
|
|
|
## Configuración
|
|
|
|
### Parámetros de Descarga
|
|
En `DownloadManager`:
|
|
```swift
|
|
private let maxConcurrentDownloads = 3 // Máximo de capítulos simultáneos
|
|
private let maxConcurrentImagesPerChapter = 5 // Máximo de imágenes simultáneas por capítulo
|
|
```
|
|
|
|
### Calidad de Imagen
|
|
En `StorageService.saveImage()`:
|
|
```swift
|
|
image.jpegData(compressionQuality: 0.8) // 80% de calidad JPEG
|
|
```
|
|
|
|
En `DownloadExtensions`:
|
|
```swift
|
|
func optimizedForStorage() -> Data? {
|
|
// Redimensiona si > 2048px
|
|
// Comprime a 75% de calidad
|
|
}
|
|
```
|
|
|
|
## Integración con ReaderView
|
|
|
|
Para leer capítulos descargados:
|
|
|
|
```swift
|
|
struct ReaderView: View {
|
|
let chapter: Chapter
|
|
let mangaSlug: String
|
|
|
|
@StateObject private var storage = StorageService.shared
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVStack {
|
|
ForEach(pageIndices, id: \.self) { index in
|
|
if let imageURL = storage.getImageURL(
|
|
mangaSlug: mangaSlug,
|
|
chapterNumber: chapter.number,
|
|
pageIndex: index
|
|
) {
|
|
// Usar imagen local
|
|
AsyncImage(url: imageURL) { image in
|
|
image.resizable()
|
|
} placeholder: {
|
|
ProgressView()
|
|
}
|
|
} else {
|
|
// Fallback a URL remota
|
|
RemoteChapterPage(url: remoteURL)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Notificaciones
|
|
|
|
El sistema emite notificaciones para seguimiento:
|
|
```swift
|
|
extension Notification.Name {
|
|
static let downloadDidStart = Notification.Name("downloadDidStart")
|
|
static let downloadDidUpdate = Notification.Name("downloadDidUpdate")
|
|
static let downloadDidComplete = Notification.Name("downloadDidComplete")
|
|
static let downloadDidFail = Notification.Name("downloadDidFail")
|
|
static let downloadDidCancel = Notification.Name("downloadDidCancel")
|
|
}
|
|
```
|
|
|
|
## Manejo de Errores
|
|
|
|
### Errores de Red
|
|
- Timeout: 30 segundos por imagen
|
|
- Reintentos: Manejados por URLSession
|
|
- HTTP errors: Capturados y reportados en UI
|
|
|
|
### Errores de Almacenamiento
|
|
- Espacio insuficiente: Error con mensaje descriptivo
|
|
- Permisos: Manejados por FileManager
|
|
- Corrupción de archivos: Archivos eliminados y descarga reiniciada
|
|
|
|
### Errores de Scraping
|
|
- No se encontraron imágenes: Error `noImagesFound`
|
|
- Página no carga: Error del scraper propagado
|
|
- Cambios en la web: Requieren actualización del scraper
|
|
|
|
## Best Practices
|
|
|
|
### 1. Concurrencia
|
|
El sistema usa Swift Concurrency:
|
|
- `async/await` para operaciones asíncronas
|
|
- `Task` para crear contextos de concurrencia
|
|
- `@MainActor` para actualizaciones de UI
|
|
- `TaskGroup` para descargas en paralelo
|
|
|
|
### 2. Memoria
|
|
- Imágenes comprimidas antes de guardar
|
|
- Descarga limitada a 5 imágenes simultáneas
|
|
- Limpieza automática de historiales (50 completadas, 20 fallidas)
|
|
|
|
### 3. UX
|
|
- Progreso visible en tiempo real
|
|
- Cancelación en cualquier punto
|
|
- Notificaciones de estado
|
|
- Estados vacíos descriptivos
|
|
- Feedback inmediato de acciones
|
|
|
|
### 4. Robustez
|
|
- Validación de estados antes de descargar
|
|
- Limpieza de archivos parciales al cancelar
|
|
- Verificación de archivos existentes
|
|
- Manejo exhaustivo de errores
|
|
|
|
## Testing
|
|
|
|
### Pruebas Unitarias
|
|
```swift
|
|
func testDownloadManager() async throws {
|
|
let manager = DownloadManager.shared
|
|
|
|
// Probar descarga individual
|
|
try await manager.downloadChapter(
|
|
mangaSlug: "test",
|
|
mangaTitle: "Test Manga",
|
|
chapter: testChapter
|
|
)
|
|
|
|
XCTAssertTrue(manager.activeDownloads.isEmpty)
|
|
XCTAssertEqual(manager.completedDownloads.count, 1)
|
|
}
|
|
```
|
|
|
|
### Pruebas de Integración
|
|
- Descargar capítulo completo
|
|
- Cancelar descarga a mitad
|
|
- Descargar múltiples capítulos
|
|
- Probar con y sin conexión
|
|
- Verificar persistencia de archivos
|
|
|
|
## Troubleshooting
|
|
|
|
### Descargas no inician
|
|
- Verificar conexión a internet
|
|
- Verificar que el scraper puede acceder a la web
|
|
- Revisar logs del scraper
|
|
|
|
### Progreso no actualiza
|
|
- Asegurar que las vistas están en @MainActor
|
|
- Verificar que DownloadTask es @ObservedObject
|
|
- Chequear que las propiedades son @Published
|
|
|
|
### Archivos no se guardan
|
|
- Verificar permisos de la app
|
|
- Chequear espacio disponible
|
|
- Revisar que directorios existen
|
|
|
|
### Imágenes corruptas
|
|
- Verificar calidad de compresión
|
|
- Chequear que URLs sean válidas
|
|
- Probar redimensionado de imágenes
|
|
|
|
## Futuras Mejoras
|
|
|
|
- [ ] Soporte para reanudar descargas pausadas
|
|
- [ ] Priorización de descargas
|
|
- [ ] Descarga automática de nuevos capítulos
|
|
- [ ] Compresión adicional de imágenes
|
|
- [ ] Soporte para formatos WebP
|
|
- [ ] Batch operations en StorageService
|
|
- [ ] Background downloads con URLSession
|
|
- [ ] Metrics y analytics de descargas
|