✨ 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>
9.1 KiB
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:
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:
- Usuario toca botón de descarga en toolbar → muestra alert
- Selecciona cantidad de capítulos a descargar
- Cada capítulo muestra progreso de descarga en tiempo real
- Notificación aparece al completar todas las descargas
- 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:
// 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:
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:
enum DownloadState {
case pending
case downloading(progress: Double)
case completed
case failed(error: String)
case cancelled
}
DownloadError
Tipos de errores de descarga:
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:
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():
image.jpegData(compressionQuality: 0.8) // 80% de calidad JPEG
En DownloadExtensions:
func optimizedForStorage() -> Data? {
// Redimensiona si > 2048px
// Comprime a 75% de calidad
}
Integración con ReaderView
Para leer capítulos descargados:
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:
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/awaitpara operaciones asíncronasTaskpara crear contextos de concurrencia@MainActorpara actualizaciones de UITaskGrouppara 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
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